Adding embedded dbwire library

This commit is contained in:
Miroslav Štampar 2026-07-12 10:32:57 +02:00
parent d5ff557a13
commit 921870ccf0
13 changed files with 1682 additions and 3 deletions

View file

@ -222,7 +222,7 @@
</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 %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"/>
<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>

50
extra/dbwire/__init__.py Normal file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
"""
dbwire - minimal, dependency-free (stdlib-only) database wire-protocol clients used as a fallback for
sqlmap's direct ('-d') connection when no native driver (and no SQLAlchemy) is installed.
Design note: connectors speak a *wire protocol*, not a product, so a single client covers the whole
compatible family - e.g. the PostgreSQL client also serves CockroachDB, CrateDB, Redshift and Greenplum;
a MySQL client serves MariaDB/TiDB/Aurora; a TDS client serves MSSQL/Sybase. Each module exposes a small
PEP 249 (DB-API 2.0) subset (connect(), Connection.cursor()/commit()/close(), Cursor.execute()/fetchall()).
"""
__version__ = "0.1"
apilevel = "2.0"
threadsafety = 1
paramstyle = "pyformat"
# PEP 249 exception hierarchy (shared by every wire module)
class Error(Exception):
pass
class InterfaceError(Error):
pass
class DatabaseError(Error):
pass
class OperationalError(DatabaseError):
pass
class DataError(DatabaseError):
pass
class IntegrityError(DatabaseError):
pass
class ProgrammingError(DatabaseError):
pass
class InternalError(DatabaseError):
pass
class NotSupportedError(DatabaseError):
pass

126
extra/dbwire/clickhouse.py Normal file
View file

@ -0,0 +1,126 @@
#!/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
try:
from urllib.request import Request, urlopen # Python 3
from urllib.error import HTTPError, URLError
except ImportError:
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
from extra.dbwire import OperationalError
from extra.dbwire import ProgrammingError
def _unescape(value):
if value == "\\N":
return None
if "\\" not in value:
return value
out, it = [], iter(range(len(value)))
i = 0
n = len(value)
while i < n:
ch = value[i]
if ch == "\\" and i + 1 < n:
nxt = value[i + 1]
out.append({"t": "\t", "n": "\n", "r": "\r", "0": "\0", "\\": "\\", "'": "'"}.get(nxt, nxt))
i += 2
else:
out.append(ch)
i += 1
return "".join(out)
class Cursor(object):
def __init__(self, connection):
self.connection = connection
self.description = None
self.rowcount = -1
self._rows = []
self._pos = 0
def execute(self, query, params=None):
if params is not None:
raise ProgrammingError("parameter binding is not supported; pass a fully-formed query string")
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
self.description, self._rows = self.connection._query(query)
self.rowcount = len(self._rows)
return self
def fetchall(self):
retVal = self._rows[self._pos:]
self._pos = len(self._rows)
return retVal
def fetchone(self):
if self._pos >= len(self._rows):
return None
retVal = self._rows[self._pos]
self._pos += 1
return retVal
def close(self):
self._rows = []
class Connection(object):
def __init__(self, host, port, user, password, database, timeout):
self._url = "http://%s:%d/?database=%s&default_format=TabSeparatedWithNames" % (host, port, database or "default")
self._headers = {}
if user or password:
token = base64.b64encode(("%s:%s" % (user or "", password or "")).encode("utf-8")).decode("ascii")
self._headers["Authorization"] = "Basic %s" % token
self._timeout = timeout
def cursor(self):
return Cursor(self)
def commit(self):
pass # ClickHouse statements are executed immediately (no client-side transaction)
def rollback(self):
pass
def close(self):
pass # HTTP is stateless
def _query(self, query):
req = Request(self._url, data=query.encode("utf-8"), headers=self._headers)
try:
body = urlopen(req, timeout=self._timeout).read().decode("utf-8", "replace")
except HTTPError as ex:
raise ProgrammingError("(remote) %s" % ex.read().decode("utf-8", "replace").strip())
except URLError as ex:
raise OperationalError("(remote) %s" % ex)
if not body:
return None, []
lines = body.split("\n")
if lines and lines[-1] == "":
lines.pop()
if not lines:
return None, []
description = [(name, None, None, None, None, None, None) for name in (_unescape(_) for _ in lines[0].split("\t"))]
rows = [tuple(_unescape(_) for _ in line.split("\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

231
extra/dbwire/monetdb.py Normal file
View file

@ -0,0 +1,231 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
"""
Minimal pure-python MonetDB MAPI client (stdlib only, no pymonetdb).
MAPI is a block-framed text protocol: a 2-byte little-endian header (length << 1 | last-flag) wraps each
block; login is a colon-separated challenge/response with a chosen password hash; queries are sent with an
's' prefix and results come back as '&' headers, '%' metadata and '[' tuples. Paging is disabled up front
(Xreply_size -1) so a whole result set arrives at once.
"""
import hashlib
import re
import socket
import struct
from extra.dbwire import InterfaceError
from extra.dbwire import NotSupportedError
from extra.dbwire import OperationalError
from extra.dbwire import ProgrammingError
_MAX_BLOCK = 0xffff >> 1
def _recvn(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise InterfaceError("connection closed by server")
buf += chunk
return buf
def _getblock(sock):
out = b""
while True:
(header,) = struct.unpack("<H", _recvn(sock, 2))
length, last = header >> 1, header & 1
out += _recvn(sock, length)
if last:
break
return out.decode("utf-8", "replace")
def _putblock(sock, text):
data = text.encode("utf-8")
off = 0
while True:
chunk = data[off:off + _MAX_BLOCK]
off += _MAX_BLOCK
last = off >= len(data)
sock.sendall(struct.pack("<H", (len(chunk) << 1) | (1 if last else 0)) + chunk)
if last:
break
def _challenge_response(challenge, user, password, database):
parts = challenge.split(":")
if len(parts) < 7 or parts[-1] != "":
raise OperationalError("invalid MonetDB challenge")
salt, server_type, protocol, hashes = parts[0], parts[1], parts[2], parts[3]
if protocol != "9":
raise NotSupportedError("only MAPI protocol v9 is supported")
if server_type == "merovingian": # proxy stage: authenticate as merovingian, real creds go to the mserver
user, password = "merovingian", ""
pwalgo = parts[5]
try:
password = hashlib.new(pwalgo, password.encode("utf-8")).hexdigest()
except ValueError:
raise NotSupportedError("unsupported MonetDB password algorithm: %s" % pwalgo)
pwhash = None
for algo in hashes.split(","):
try:
h = hashlib.new(algo)
except ValueError:
continue
h.update(password.encode("utf-8"))
h.update(salt.encode("utf-8"))
pwhash = "{%s}%s" % (algo, h.hexdigest())
break
if pwhash is None:
raise NotSupportedError("no supported MonetDB password hash in: %s" % hashes)
return ":".join(["BIG", user, pwhash, "sql", database or ""]) + ":"
def _unquote(value):
if value == "NULL":
return None
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
body = value[1:-1]
if "\\" not in body:
return body
# MonetDB renders control bytes as C octal escapes (\ooo) etc.; decode with unicode_escape but only
# on the ASCII runs so raw multibyte (> 0x7f) is preserved (mirrors pymonetdb's result decoding)
return "".join(seg.encode("utf-8").decode("unicode_escape") if "\\" in seg else seg
for seg in re.split(r"([\x00-\x7f]+)", body))
return value
def _parse_result(text):
description, rows = None, []
for line in text.split("\n"):
if not line:
continue
marker = line[0]
if marker == "!": # error
raise ProgrammingError("(remote) %s" % line[1:].strip())
elif marker == "%": # metadata: "<values> # <kind>"
payload, _, kind = line[1:].rpartition("#")
if kind.strip() == "name":
description = [(name.strip(), None, None, None, None, None, None) for name in payload.split(",\t")]
elif marker == "[": # tuple: "[ v1,\tv2,\t... ]"
body = line.strip()
if body.startswith("[") and body.endswith("]"):
body = body[1:-1].strip()
rows.append(tuple(_unquote(v.strip()) for v in body.split(",\t")))
# "&" result headers, "#" info, "=" no-slice tuples are ignored for our purposes
return description, rows
class Cursor(object):
def __init__(self, connection):
self.connection = connection
self.description = None
self.rowcount = -1
self._rows = []
self._pos = 0
def execute(self, query, params=None):
if params is not None:
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
self.description, self._rows = self.connection._query(query)
self.rowcount = len(self._rows)
return self
def fetchall(self):
retVal = self._rows[self._pos:]
self._pos = len(self._rows)
return retVal
def fetchone(self):
if self._pos >= len(self._rows):
return None
retVal = self._rows[self._pos]
self._pos += 1
return retVal
def close(self):
self._rows = []
class Connection(object):
def __init__(self, sock):
self._sock = sock
def cursor(self):
return Cursor(self)
def commit(self):
pass # sqlmap runs autonomous statements; MonetDB SQL is auto-committed unless a transaction is opened
def rollback(self):
pass
def close(self):
try:
self._sock.close()
except Exception:
pass
def _command(self, text):
_putblock(self._sock, text)
reply = _getblock(self._sock)
if reply.startswith("!"):
raise OperationalError("(remote) %s" % reply[1:].strip())
def _query(self, query):
try:
_putblock(self._sock, "s" + query + ";\n")
return _parse_result(_getblock(self._sock))
except (socket.error, socket.timeout) as ex:
raise OperationalError("connection error: %s" % ex)
except (struct.error, IndexError, ValueError) as ex:
raise InterfaceError("malformed server response: %s" % ex)
def connect(host=None, port=50000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
host, port = host or "localhost", int(port or 50000)
try:
sock = socket.create_connection((host, port), timeout=connect_timeout)
sock.settimeout(None)
except (socket.error, socket.timeout) as ex:
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
try:
for _ in range(10): # bounded: merovingian proxy stage + optional redirect + mserver challenge
block = _getblock(sock)
if block == "": # login accepted
break
if block[0] == "^": # redirect
url = block[1:].strip()
m = re.match(r"mapi:monetdb://([^:/]+):(\d+)/(\S*)", url)
if m and (m.group(1) != host or int(m.group(2)) != port):
sock.close()
host, port, database = m.group(1), int(m.group(2)), m.group(3) or database
sock = socket.create_connection((host, port), timeout=connect_timeout)
sock.settimeout(None)
continue # merovingian proxy redirect: keep reading the next challenge on this socket
if block[0] == "!":
raise OperationalError("(remote) %s" % block[1:].strip())
_putblock(sock, _challenge_response(block, user, password, database))
else:
raise OperationalError("MonetDB login did not converge")
except (OperationalError, NotSupportedError, InterfaceError):
try:
sock.close()
except Exception:
pass
raise
except (socket.error, socket.timeout) as ex: # I/O error during the login/redirect exchange
try:
sock.close()
except Exception:
pass
raise OperationalError("connection error: %s" % ex)
connection = Connection(sock)
try:
connection._command("Xreply_size -1\n") # disable row paging so a whole result set is returned at once
except (socket.error, socket.timeout) as ex:
connection.close()
raise OperationalError("connection error: %s" % ex)
return connection

335
extra/dbwire/mysql.py Normal file
View file

@ -0,0 +1,335 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
"""
Minimal pure-python MySQL client/server protocol client (stdlib only).
Covers the whole MySQL-wire family (MySQL, MariaDB, TiDB, Aurora-MySQL, Percona, ...). Auth:
mysql_native_password (full), plus caching_sha2_password fast path; caching_sha2 *full* auth over a
plaintext connection needs RSA (not in the stdlib), so that case raises a clean NotSupportedError - use a
mysql_native_password account (as MariaDB/TiDB default to) for the dependency-free path.
"""
import hashlib
import socket
import struct
from extra.dbwire import DatabaseError
from extra.dbwire import InterfaceError
from extra.dbwire import NotSupportedError
from extra.dbwire import OperationalError
from extra.dbwire import ProgrammingError
# capability flags
_CLIENT_LONG_PASSWORD = 0x00000001
_CLIENT_LONG_FLAG = 0x00000004
_CLIENT_CONNECT_WITH_DB = 0x00000008
_CLIENT_PROTOCOL_41 = 0x00000200
_CLIENT_TRANSACTIONS = 0x00002000
_CLIENT_SECURE_CONNECTION = 0x00008000
_CLIENT_PLUGIN_AUTH = 0x00080000
_MAX_PACKET = 0x1000000
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) payload, to bound a hostile/corrupt stream
_BINARY_CHARSET = 63 # collation id 63 == 'binary' (BLOB/BINARY/VARBINARY columns)
def _xor(a, b):
if str is bytes: # Python 2
return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b))
return bytes(x ^ y for x, y in zip(a, b))
def _u8(data, off):
return struct.unpack("<B", data[off:off + 1])[0]
def _cstring(data, off):
# NUL-terminated string, tolerant of a missing terminator (returns the remainder)
end = data.find(b"\x00", off)
if end == -1:
return data[off:], len(data)
return data[off:end], end + 1
def _recvn(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise InterfaceError("connection closed by server")
buf += chunk
return buf
def _read_packet(sock):
header = _recvn(sock, 4)
length = struct.unpack("<I", header[0:3] + b"\x00")[0]
seq = _u8(header, 3)
payload = _recvn(sock, length)
total = length
while length == 0xffffff: # payload continues in the next packet
header = _recvn(sock, 4)
length = struct.unpack("<I", header[0:3] + b"\x00")[0]
total += length
if total > _MAX_MESSAGE_LENGTH:
raise InterfaceError("backend message too large (%d bytes)" % total)
payload += _recvn(sock, length)
return seq, payload
def _send_packet(sock, seq, payload):
while True: # split payloads >= 16 MB into 0xffffff-sized packets (with a trailing short packet)
chunk = payload[:0xffffff]
sock.sendall(struct.pack("<I", len(chunk))[0:3] + struct.pack("<B", seq & 0xff) + chunk)
seq = (seq + 1) & 0xff
payload = payload[0xffffff:]
if len(chunk) < 0xffffff:
break
def _lenc_int(data, off):
first = _u8(data, off)
if first < 0xfb:
return first, off + 1
elif first == 0xfb:
return None, off + 1 # NULL (in a row)
elif first == 0xfc:
return struct.unpack("<H", data[off + 1:off + 3])[0], off + 3
elif first == 0xfd:
return struct.unpack("<I", data[off + 1:off + 4] + b"\x00")[0], off + 4
else: # 0xfe
return struct.unpack("<Q", data[off + 1:off + 9])[0], off + 9
def _lenc_str(data, off):
length, off = _lenc_int(data, off)
if length is None:
return None, off
if off + length > len(data):
raise InterfaceError("length-encoded string overruns packet")
return data[off:off + length], off + length
def _err_message(payload):
# ERR packet: 0xff, Int2 code, (if PROTOCOL_41) '#' + 5-byte SQLSTATE, then message
off = 3
if payload[3:4] == b"#":
off = 9
return payload[off:].decode("utf-8", "replace")
def _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]
description.append((name.decode("utf-8", "replace"), None, None, None, None, None, None))
binary.append(charset == _BINARY_CHARSET)
_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 binary[i]:
row.append(value) # keep binary/BLOB columns as raw bytes (sqlmap hex-encodes them)
else:
row.append(value.decode("utf-8", "replace"))
rows.append(tuple(row))
return description, rows, len(rows)
def _finish_auth(sock, password, plugin, salt):
# read the auth result, handling AuthSwitchRequest (0xfe) and AuthMoreData (0x01) for caching_sha2
while True:
seq, payload = _read_packet(sock)
marker = _u8(payload, 0)
if marker == 0x00: # OK
return
if marker == 0xff: # ERR
raise OperationalError("(remote) %s" % _err_message(payload))
if marker == 0xfe: # AuthSwitchRequest: <plugin name>\x00<salt>
plugin, off = _cstring(payload, 1)
plugin = plugin.decode("ascii", "replace")
salt = payload[off:].rstrip(b"\x00")
if plugin == "mysql_native_password":
data = _scramble_native(password, salt)
elif plugin == "caching_sha2_password":
data = _scramble_sha2(password, salt)
else:
raise NotSupportedError("unsupported authentication plugin '%s'" % plugin)
_send_packet(sock, seq + 1, data)
elif marker == 0x01: # AuthMoreData (caching_sha2)
status = _u8(payload, 1)
if status == 0x03: # fast auth success -> OK packet follows
continue
elif status == 0x04: # full auth required (needs TLS or RSA - not available stdlib-only)
raise NotSupportedError("caching_sha2_password full authentication over a plaintext connection "
"requires RSA/TLS; use a mysql_native_password account for the dependency-free client")
else:
raise OperationalError("unexpected caching_sha2 auth status %d" % status)
else:
raise InterfaceError("unexpected authentication response 0x%02x" % marker)
def connect(host=None, port=3306, user=None, password=None, database=None, connect_timeout=None, **kwargs):
try:
sock = socket.create_connection((host or "localhost", int(port or 3306)), timeout=connect_timeout)
sock.settimeout(None)
except (socket.error, socket.timeout) as ex:
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
try:
seq, payload = _read_packet(sock)
if _u8(payload, 0) == 0xff:
raise OperationalError("(remote) %s" % _err_message(payload))
off = 1 # protocol version (10)
_, off = _cstring(payload, off) # server version
off += 4 # connection id
salt = payload[off:off + 8]; off += 8 + 1 # auth-plugin-data part 1 (+ filler)
off += 2 # capability flags (lower)
off += 1 # character set
off += 2 # status flags
off += 2 # capability flags (upper)
auth_data_len = _u8(payload, off); off += 1
off += 10 # reserved
salt += payload[off:off + max(13, auth_data_len - 8) - 1] # part 2 (drop trailing NUL)
off += max(13, auth_data_len - 8)
plugin = "mysql_native_password"
if off < len(payload):
name, _ = _cstring(payload, off)
plugin = name.decode("ascii", "replace") or plugin
if plugin == "caching_sha2_password":
auth_response = _scramble_sha2(password or "", salt)
else:
plugin = "mysql_native_password"
auth_response = _scramble_native(password or "", salt)
flags = (_CLIENT_LONG_PASSWORD | _CLIENT_LONG_FLAG | _CLIENT_PROTOCOL_41 |
_CLIENT_TRANSACTIONS | _CLIENT_SECURE_CONNECTION | _CLIENT_PLUGIN_AUTH)
if database:
flags |= _CLIENT_CONNECT_WITH_DB
response = struct.pack("<I", flags) + struct.pack("<I", _MAX_PACKET) + struct.pack("<B", 45) + (b"\x00" * 23)
response += (user or "").encode("utf-8") + b"\x00"
response += struct.pack("<B", len(auth_response)) + auth_response
if database:
response += database.encode("utf-8") + b"\x00"
response += plugin.encode("ascii") + b"\x00"
_send_packet(sock, seq + 1, response)
_finish_auth(sock, password or "", plugin, salt)
except (DatabaseError, InterfaceError):
_safe_close(sock)
raise
except Exception as ex:
_safe_close(sock)
raise OperationalError("handshake failed (%s)" % ex)
connection = Connection(sock)
try:
connection._query("SET autocommit=1") # so DML persists even if the server default is autocommit=0
except Exception:
pass
return connection
def _safe_close(sock):
try:
sock.close()
except Exception:
pass

265
extra/dbwire/postgres.py Normal file
View file

@ -0,0 +1,265 @@
#!/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, ...).
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 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 ProgrammingError
_PROTOCOL_VERSION = 196608 # 3.0
_MAX_MESSAGE_LENGTH = 0x40000000 # 1 GB - guard against a hostile/corrupt length triggering an unbounded read
# SQLSTATE class (first 2 chars) -> DB-API exception, so callers can distinguish (mirrors psycopg2)
_SQLSTATE_CLASS = {
"22": DataError, "23": IntegrityError,
"08": OperationalError, "28": OperationalError, "53": OperationalError,
"57": OperationalError, "58": OperationalError,
}
def _xor(a, b):
# byte-wise XOR of two equal-length byte strings (Python 2 and 3 safe)
if str is bytes: # Python 2: iterating bytes yields 1-char strings
return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b))
return bytes(x ^ y for x, y in zip(a, b))
def _recvn(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise InterfaceError("connection closed by server")
buf += chunk
return buf
def _read_message(sock):
mtype = _recvn(sock, 1)
(length,) = struct.unpack("!I", _recvn(sock, 4))
if length < 4 or length > _MAX_MESSAGE_LENGTH:
raise InterfaceError("invalid backend message length (%d)" % length)
return mtype, _recvn(sock, length - 4)
def _send(sock, mtype, payload):
sock.sendall((mtype or b"") + struct.pack("!I", len(payload) + 4) + payload)
def _error_message(payload):
# ErrorResponse/NoticeResponse: series of (byte field-code, cstring value), terminated by a NUL byte.
# Returns (human message, SQLSTATE). Tolerant of a truncated/unterminated stream (find() not index()).
fields, off = {}, 0
while off < len(payload) and payload[off:off + 1] != b"\x00":
code = payload[off:off + 1]
end = payload.find(b"\x00", off + 1)
if end == -1:
break
fields[code] = payload[off + 1:end].decode("utf-8", "replace")
off = end + 1
return fields.get(b"M", "unknown error"), fields.get(b"C", "")
def _raise_server_error(message, sqlstate):
raise _SQLSTATE_CLASS.get((sqlstate or "")[:2], ProgrammingError)("(remote) %s" % message)
class Cursor(object):
def __init__(self, connection):
self.connection = connection
self.description = None
self.rowcount = -1
self._rows = []
self._pos = 0
def execute(self, query, params=None):
if params is not None:
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0 # reset before (a failed) query
self.description, self._rows, self._pos, self.rowcount = self.connection._simple_query(query)
return self
def fetchall(self):
retVal = self._rows[self._pos:]
self._pos = len(self._rows)
return retVal
def fetchone(self):
if self._pos >= len(self._rows):
return None
retVal = self._rows[self._pos]
self._pos += 1
return retVal
def close(self):
self._rows = []
class Connection(object):
def __init__(self, sock):
self._sock = sock
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 _simple_query(self, query):
_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 _ 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")
row.append(payload[off:off + vlen].decode("utf-8", "replace"))
off += vlen
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)
break
# ParameterStatus(S)/NoticeResponse(N)/EmptyQueryResponse(I)/CopyData(d)/CopyDone(c)/... ignored
except (struct.error, IndexError, ValueError) as ex:
raise InterfaceError("malformed backend message: %s" % ex)
if error is not None:
_raise_server_error(*error)
return description, rows, 0, rowcount
def _authenticate(sock, user, password):
cfirst_bare = None
while True:
mtype, payload = _read_message(sock)
if mtype in (b"N", b"S"): # NoticeResponse / ParameterStatus may legally precede AuthenticationOk
continue
if mtype == b"E":
_raise_server_error_as_operational(payload)
if mtype != b"R":
raise InterfaceError("unexpected message %r during authentication" % mtype)
(code,) = struct.unpack("!I", payload[:4])
if code == 0: # AuthenticationOk (also the trust case)
return
elif code == 3: # cleartext password
_send(sock, b"p", (password or "").encode("utf-8") + b"\x00")
elif code == 5: # MD5 password
salt = payload[4:8]
inner = hashlib.md5((password or "").encode("utf-8") + (user or "").encode("utf-8")).hexdigest()
token = b"md5" + hashlib.md5(inner.encode("ascii") + salt).hexdigest().encode("ascii")
_send(sock, b"p", token + b"\x00")
elif code == 10: # SASL (SCRAM-SHA-256)
if not hasattr(hashlib, "pbkdf2_hmac"):
raise NotSupportedError("SCRAM-SHA-256 authentication requires Python >= 2.7.8 (hashlib.pbkdf2_hmac)")
nonce = base64.b64encode(os.urandom(18)).decode("ascii")
cfirst_bare = "n=,r=%s" % nonce
client_first = "n,," + cfirst_bare
_send(sock, b"p", b"SCRAM-SHA-256\x00" + struct.pack("!I", len(client_first)) + client_first.encode("ascii"))
elif code == 11: # SASLContinue (server-first)
try:
server_first = payload[4:].decode("ascii")
attrs = dict(kv.split("=", 1) for kv in server_first.split(","))
snonce, salt, iterations = attrs["r"], base64.b64decode(attrs["s"]), int(attrs["i"])
except (KeyError, ValueError, binascii.Error, UnicodeDecodeError) as ex:
raise OperationalError("malformed SCRAM server-first message (%s)" % ex)
salted = hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, iterations)
client_key = hmac.new(salted, b"Client Key", hashlib.sha256).digest()
stored_key = hashlib.sha256(client_key).digest()
client_final_noproof = "c=biws,r=%s" % snonce
auth_message = "%s,%s,%s" % (cfirst_bare, server_first, client_final_noproof)
client_sig = hmac.new(stored_key, auth_message.encode("ascii"), hashlib.sha256).digest()
proof = base64.b64encode(_xor(client_key, client_sig)).decode("ascii")
_send(sock, b"p", ("%s,p=%s" % (client_final_noproof, proof)).encode("ascii"))
elif code == 12: # SASLFinal
pass
else:
raise InterfaceError("unsupported authentication request %d" % code)
def _raise_server_error_as_operational(payload):
message, _ = _error_message(payload)
raise OperationalError("(remote) %s" % message)
def connect(host=None, port=5432, user=None, password=None, database=None, connect_timeout=None, **kwargs):
try:
sock = socket.create_connection((host or "localhost", int(port or 5432)), timeout=connect_timeout)
sock.settimeout(None)
except (socket.error, socket.timeout) as ex:
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
params = b""
for key, value in (("user", user or ""), ("database", database or user or ""), ("client_encoding", "UTF8")):
params += key.encode("ascii") + b"\x00" + ("%s" % value).encode("utf-8") + b"\x00"
params += b"\x00"
_send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params)
try:
_authenticate(sock, user, password)
while True: # drain until ReadyForQuery (ParameterStatus/BackendKeyData/NoticeResponse)
mtype, payload = _read_message(sock)
if mtype == b"E":
_raise_server_error_as_operational(payload)
if mtype == b"Z":
break
except Exception: # any setup failure (DB-API or otherwise) must still close the socket
try:
sock.close()
except Exception:
pass
raise
return Connection(sock)

125
extra/dbwire/presto.py Normal file
View file

@ -0,0 +1,125 @@
#!/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 time
try:
from urllib.request import Request, urlopen # Python 3
from urllib.error import HTTPError, URLError
except ImportError:
from urllib2 import Request, urlopen, HTTPError, URLError # Python 2
from extra.dbwire import InterfaceError
from extra.dbwire import NotSupportedError
from extra.dbwire import OperationalError
from extra.dbwire import ProgrammingError
class Cursor(object):
def __init__(self, connection):
self.connection = connection
self.description = None
self.rowcount = -1
self._rows = []
self._pos = 0
def execute(self, query, params=None):
if params is not None:
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
self.description, self._rows = self.connection._query(query)
self.rowcount = len(self._rows)
return self
def fetchall(self):
retVal = self._rows[self._pos:]
self._pos = len(self._rows)
return retVal
def fetchone(self):
if self._pos >= len(self._rows):
return None
retVal = self._rows[self._pos]
self._pos += 1
return retVal
def close(self):
self._rows = []
class Connection(object):
def __init__(self, host, port, user, password, catalog, schema, timeout):
self._statement_url = "http://%s:%d/v1/statement" % (host, port)
self._timeout = timeout
self._headers = {"Content-Type": "text/plain"}
for prefix in ("X-Presto-", "X-Trino-"):
self._headers[prefix + "User"] = user or "sqlmap"
self._headers[prefix + "Catalog"] = catalog or ""
self._headers[prefix + "Schema"] = schema or "default"
self._headers[prefix + "Source"] = "dbwire"
if password:
token = base64.b64encode(("%s:%s" % (user or "", password)).encode("utf-8")).decode("ascii")
self._headers["Authorization"] = "Basic %s" % token
def cursor(self):
return Cursor(self)
def commit(self):
pass
def rollback(self):
pass
def close(self):
pass # HTTP is stateless
def _request(self, url, data=None):
req = Request(url, data=data.encode("utf-8") if data is not None else None, headers=self._headers)
try:
body = urlopen(req, timeout=self._timeout).read().decode("utf-8", "replace")
except HTTPError as ex:
raise ProgrammingError("(remote) HTTP %s: %s" % (ex.code, ex.read().decode("utf-8", "replace")[:200]))
except URLError as ex:
raise OperationalError("(remote) %s" % ex)
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 = 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"]]
for row in page.get("data") or []:
rows.append(tuple(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

466
extra/dbwire/tds.py Normal file
View file

@ -0,0 +1,466 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
"""
Minimal pure-python TDS (Tabular Data Stream) client for Microsoft SQL Server / Sybase (stdlib only).
Cleartext login only (TDS pre-login encryption negotiated to NOT_SUP); a server that forces encryption
would need TLS-in-TDS which is out of scope for the dependency-free client. Implements PRELOGIN, LOGIN7,
SQL batch, and decoding of the common column types (int/bit/float/money/decimal, (n)char/(n)varchar and
their MAX/PLP forms, binary, guid, datetime family) to text (binary columns are returned as raw bytes so
sqlmap hex-encodes them).
"""
import socket
import struct
from extra.dbwire import DatabaseError
from extra.dbwire import InterfaceError
from extra.dbwire import NotSupportedError
from extra.dbwire import OperationalError
from extra.dbwire import ProgrammingError
_MAX_MESSAGE_LENGTH = 0x40000000
# packet types
_PKT_SQL_BATCH = 0x01
_PKT_LOGIN7 = 0x10
_PKT_PRELOGIN = 0x12
_STATUS_EOM = 0x01
def _u8(data, off):
return struct.unpack("<B", data[off:off + 1])[0]
def _recvn(sock, n):
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise InterfaceError("connection closed by server")
buf += chunk
return buf
def _send_message(sock, mtype, data):
# split into <= 4096-byte packets (8-byte header + <=4088 data); only the last carries the EOM status bit
chunk_size = 4088
packet_id = 0
off = 0
while True:
chunk = data[off:off + chunk_size]
off += chunk_size
last = off >= len(data)
header = struct.pack(">BBHHBB", mtype, _STATUS_EOM if last else 0x00, len(chunk) + 8, 0, packet_id & 0xff, 0)
sock.sendall(header + chunk)
packet_id += 1
if last:
break
def _read_message(sock):
# reassemble a full TDS message across packets (EOM status bit marks the last)
body = b""
while True:
header = _recvn(sock, 8)
mtype, status, length = struct.unpack(">BBH", header[:4])
if length < 8 or length > _MAX_MESSAGE_LENGTH:
raise InterfaceError("invalid TDS packet length (%d)" % length)
body += _recvn(sock, length - 8)
if status & _STATUS_EOM:
break
return body
# ---- PRELOGIN ----------------------------------------------------------------------------------------
def _prelogin(sock):
ver = struct.pack(">IH", 0x11000000, 0)
enc = b"\x02" # ENCRYPT_NOT_SUP
tokens = b"\x00" + struct.pack(">HH", 11, len(ver))
tokens += b"\x01" + struct.pack(">HH", 11 + len(ver), len(enc))
tokens += b"\xff"
_send_message(sock, _PKT_PRELOGIN, tokens + ver + enc)
body = _read_message(sock)
off = 0
while off < len(body) and _u8(body, off) != 0xff:
token = _u8(body, off)
toff, tlen = struct.unpack(">HH", body[off + 1:off + 5])
if token == 0x01 and _u8(body, toff) == 0x03: # server requires encryption
raise NotSupportedError("server requires TDS encryption; the dependency-free client supports cleartext only")
off += 5
# ---- LOGIN7 ------------------------------------------------------------------------------------------
def _encode_password(password):
out = bytearray()
for b in bytearray(password.encode("utf-16-le")):
b = ((b << 4) & 0xf0) | ((b >> 4) & 0x0f)
out.append(b ^ 0xa5)
return bytes(out)
def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire"):
fields = [
hostname.encode("utf-16-le"),
(user or "").encode("utf-16-le"),
_encode_password(password or ""),
appname.encode("utf-16-le"),
b"", # server name
b"", # (extension / unused)
"dbwire".encode("utf-16-le"), # client interface name
b"", # language
(database or "").encode("utf-16-le"),
]
char_counts = [6, len(user or ""), len(password or ""), 6, 0, 0, 6, 0, len(database or "")]
base = 94 # fixed header (36) + offset/length block (58)
var, offsets, cursor = b"", b"", base
for i, data in enumerate(fields):
offsets += struct.pack("<HH", cursor if data else base, char_counts[i])
var += data
cursor += len(data)
offsets += b"\x00" * 6 # ClientID (MAC)
offsets += struct.pack("<HH", base, 0) # SSPI
offsets += struct.pack("<HH", base, 0) # AtchDBFile
offsets += struct.pack("<HH", base, 0) # ChangePassword
offsets += struct.pack("<I", 0) # cbSSPILong
header = struct.pack("<I", 0x74000004) # TDS 7.4
header += struct.pack("<I", 4096) # packet size
header += struct.pack("<I", 0) # client prog version
header += struct.pack("<I", 0) # client PID
header += struct.pack("<I", 0) # connection id
header += struct.pack("<BBBB", 0, 0, 0, 0) # option flags 1/2, type flags, option flags 3
header += struct.pack("<i", 0) # client time zone
header += struct.pack("<I", 0) # client LCID
payload = header + offsets + var
payload = struct.pack("<I", len(payload) + 4) + payload # prepend total length
_send_message(sock, _PKT_LOGIN7, payload)
_parse_tokens(sock, login=True)
# ---- token stream + type decoding --------------------------------------------------------------------
def _read_us_varchar(data, off):
(n,) = struct.unpack("<B", data[off:off + 1])
return data[off + 1:off + 1 + n * 2].decode("utf-16-le", "replace"), off + 1 + n * 2
def _decode_datetime(raw):
days, ticks = struct.unpack("<iI", raw)
import datetime
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, milliseconds=ticks * 10.0 / 3.0))
class _Column(object):
__slots__ = ("name", "type", "size", "scale", "binary")
def _parse_type_info(data, off):
col = _Column()
col.type = _u8(data, off); off += 1
col.size, col.scale, col.binary = 0, 0, False
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, 0x2e, 0x37): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID
col.size = _u8(data, off); off += 1
elif t in (0x6a, 0x6c): # DECIMALN / NUMERICN
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
off += 5 # collation
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 in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE
col.size = struct.unpack("<i", data[off:off + 4])[0]; off += 4
if t in (0x23, 0x63):
off += 5 # collation
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 "%.4f" % (struct.unpack("<i", data[off:off + 4])[0] / 10000.0), off + 4
if t == 0x3c: # MONEY (8 bytes: high 4 then low 4)
hi, lo = struct.unpack("<iI", data[off:off + 8])
return "%.4f" % (((hi << 32) | lo) / 10000.0), off + 8
if t == 0x3d: # DATETIME (8 bytes)
return _decode_datetime(data[off:off + 8]), off + 8
if t == 0x3a: # DATETIM4 / smalldatetime (2-byte days since 1900 + 2-byte minutes)
import datetime
days, mins = struct.unpack("<HH", data[off:off + 4])
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins)), 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("latin-1"), off # (var)char is the collation's single-byte codepage; latin-1 round-trips every byte losslessly
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("latin-1"), off # TEXT is single-byte codepage; latin-1 is lossless
# 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
if n == 4:
v = struct.unpack("<i", raw)[0]
else:
hi, lo = struct.unpack("<ii", raw)
v = (hi << 32) | (lo & 0xffffffff)
return "%.4f" % (v / 10000.0), off
if t in (0x6a, 0x6c): # DECIMALN / NUMERICN
sign = bytearray(raw)[0]
magnitude = 0
for b in reversed(bytearray(raw[1:])):
magnitude = (magnitude << 8) | b
value = magnitude if sign else -magnitude
if col.scale:
s = "%0*d" % (col.scale + 1, value if value >= 0 else -value)
s = ("-" if value < 0 else "") + s[:-col.scale] + "." + s[-col.scale:]
return s, off
return str(value), off
if t == 0x24: # GUID
a, b, c = struct.unpack("<IHH", raw[:8])
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:])))), off
if t == 0x6f: # DATETIMN (n=8 datetime, n=4 smalldatetime)
if n == 8:
return _decode_datetime(raw), off
import datetime
days, mins = struct.unpack("<HH", raw)
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins)), off
if t == 0x28: # DATE (3 bytes: days since 0001-01-01)
import datetime
days = struct.unpack("<I", raw[:3] + b"\x00")[0]
return "%s" % (datetime.date(1, 1, 1) + datetime.timedelta(days=days)), off
if t in (0x2a, 0x29): # DATETIME2 (time + 3-byte date) / TIME (time only), time = scaled 10^-scale s units
import datetime
date_bytes = raw[-3:] if t == 0x2a else b"\x00\x00\x00"
time_bytes = raw[:-3] if t == 0x2a else raw
days = struct.unpack("<I", date_bytes + b"\x00")[0]
ticks = struct.unpack("<Q", time_bytes + b"\x00" * (8 - len(time_bytes)))[0]
seconds = ticks / (10.0 ** (col.scale or 7))
base = datetime.datetime(1, 1, 1) + datetime.timedelta(days=days, seconds=seconds)
return ("%s" % base if t == 0x2a else "%s" % base.time()), off
# unknown date/time layout: return the raw hex as a last resort (never desyncs)
return "".join("%02x" % x for x in bytearray(raw)), off
def _parse_tokens(sock, login=False):
data = _read_message(sock)
off, columns, rows, description, error = 0, [], [], None, None
while off < len(data):
token = _u8(data, off); off += 1
if token == 0x81: # COLMETADATA (a new result set: drop any prior rows so only the last is returned)
(count,) = struct.unpack("<H", data[off:off + 2]); off += 2
columns, rows = [], []
if count == 0xffff:
continue
for _ in range(count):
off += 4 # user type
off += 2 # flags
col, off = _parse_type_info(data, off)
if col.type in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE carry a table name before the col name
(numparts,) = struct.unpack("<B", data[off:off + 1]); off += 1
for _ in range(numparts):
(plen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + plen * 2
col.name, off = _read_us_varchar(data, off)
columns.append(col)
description = [(c.name, c.type, None, None, None, None, None) for c in columns]
elif token == 0xd1: # ROW
row = []
for col in columns:
value, off = _decode_value(col, data, off)
row.append(value)
rows.append(tuple(row))
elif token == 0xd2: # NBCROW (null-bitmap compressed)
nbc_len = (len(columns) + 7) // 8
bitmap = bytearray(data[off:off + nbc_len]); off += nbc_len
row = []
for i, col in enumerate(columns):
if bitmap[i // 8] & (1 << (i % 8)):
row.append(None)
else:
value, off = _decode_value(col, data, off)
row.append(value)
rows.append(tuple(row))
elif token == 0xaa: # ERROR
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2
number = struct.unpack("<i", data[off:off + 4])[0]
msg_off = off + 4 + 1 + 1 # number(4) state(1) class(1)
(mlen,) = struct.unpack("<H", data[msg_off:msg_off + 2])
error = data[msg_off + 2:msg_off + 2 + mlen * 2].decode("utf-16-le", "replace")
off += tlen
elif token == 0xab: # INFO
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
elif token == 0xad: # LOGINACK
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
elif token == 0xe3: # ENVCHANGE
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
elif token == 0x79: # RETURNSTATUS
off += 4
elif token == 0xa9: # ORDER
(tlen,) = struct.unpack("<H", data[off:off + 2]); off += 2 + tlen
elif token in (0xfd, 0xfe, 0xff): # DONE / DONEPROC / DONEINPROC
off += 12
else:
raise InterfaceError("unexpected TDS token 0x%02x" % token)
if error is not None:
if login:
raise OperationalError("(remote) %s" % error)
raise ProgrammingError("(remote) %s" % error)
return description, rows
class Cursor(object):
def __init__(self, connection):
self.connection = connection
self.description = None
self.rowcount = -1
self._rows = []
self._pos = 0
def execute(self, query, params=None):
if params is not None:
raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
self.description, self._rows = self.connection._query(query)
self.rowcount = len(self._rows)
return self
def fetchall(self):
retVal = self._rows[self._pos:]
self._pos = len(self._rows)
return retVal
def fetchone(self):
if self._pos >= len(self._rows):
return None
retVal = self._rows[self._pos]
self._pos += 1
return retVal
def close(self):
self._rows = []
class Connection(object):
def __init__(self, sock):
self._sock = sock
def cursor(self):
return Cursor(self)
def commit(self):
pass # sqlmap issues autonomous statements; SET IMPLICIT_TRANSACTIONS is off by default
def rollback(self):
pass
def close(self):
try:
self._sock.close()
except Exception:
pass
def _query(self, query):
# TDS 7.2+ SQL batch must be prefixed with ALL_HEADERS carrying the transaction descriptor header
headers = struct.pack("<I", 22) + struct.pack("<I", 18) + struct.pack("<H", 2) + struct.pack("<Q", 0) + struct.pack("<I", 1)
_send_message(self._sock, _PKT_SQL_BATCH, headers + query.encode("utf-16-le"))
try:
return _parse_tokens(self._sock)
except (struct.error, IndexError, ValueError, KeyError) as ex:
raise InterfaceError("malformed server response: %s" % ex)
def connect(host=None, port=1433, user=None, password=None, database=None, connect_timeout=None, **kwargs):
try:
sock = socket.create_connection((host or "localhost", int(port or 1433)), timeout=connect_timeout)
sock.settimeout(None)
except (socket.error, socket.timeout) as ex:
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
try:
_prelogin(sock)
_login7(sock, user, password, database)
except (DatabaseError, InterfaceError):
try:
sock.close()
except Exception:
pass
raise
except Exception as ex:
try:
sock.close()
except Exception:
pass
raise OperationalError("TDS login failed (%s)" % ex)
return Connection(sock)

View file

@ -12,7 +12,9 @@ from lib.core.data import conf
from lib.core.data import kb
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
@ -158,7 +160,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

View file

@ -71,6 +71,7 @@ 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 OBSOLETE_OPTIONS
@ -1755,6 +1756,8 @@ def parseTargetDirect():
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 "

View file

@ -258,6 +258,20 @@ DBMS_DICT = {
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: "tds",
DBMS.CLICKHOUSE: "clickhouse",
DBMS.MONETDB: "monetdb",
DBMS.PRESTO: "presto",
}
# Reference: https://blog.jooq.org/tag/sysibm-sysdummy1/
FROM_DUMMY_TABLE = {
DBMS.ORACLE: " FROM DUAL",

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.84"
VERSION = "1.10.7.85"
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)

57
lib/utils/dbwire.py Normal file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission
"""
import importlib
import logging
import extra.dbwire
from lib.core.common import getSafeExString
from lib.core.data import conf
from lib.core.data import logger
from lib.core.exception import SqlmapConnectionException
from plugins.generic.connector import Connector as GenericConnector
class Connector(GenericConnector):
"""
Adapter exposing sqlmap's connector interface over a dependency-free 'extra/dbwire' pure-python
wire-protocol client. Used for '-d' when neither a native driver nor SQLAlchemy is available.
"""
def __init__(self, module):
GenericConnector.__init__(self)
self._driver = importlib.import_module("extra.dbwire.%s" % module)
def connect(self):
self.initConnection()
try:
self.connector = self._driver.connect(host=self.hostname, port=self.port, user=self.user, password=self.password, database=self.db, connect_timeout=conf.timeout)
except extra.dbwire.Error as ex:
raise SqlmapConnectionException(getSafeExString(ex))
self.initCursor()
self.printConnected()
def fetchall(self):
try:
return self.cursor.fetchall()
except extra.dbwire.Error as ex:
logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex))
return None
def execute(self, query):
try:
self.cursor.execute(query)
except extra.dbwire.Error as ex:
logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex))
self.connector.commit()
def select(self, query):
self.execute(query)
return self.fetchall()