mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
Adding embedded dbwire library
This commit is contained in:
parent
d5ff557a13
commit
921870ccf0
13 changed files with 1682 additions and 3 deletions
|
|
@ -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
50
extra/dbwire/__init__.py
Normal 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
126
extra/dbwire/clickhouse.py
Normal 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
231
extra/dbwire/monetdb.py
Normal 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
335
extra/dbwire/mysql.py
Normal 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
265
extra/dbwire/postgres.py
Normal 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
125
extra/dbwire/presto.py
Normal 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
466
extra/dbwire/tds.py
Normal 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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
57
lib/utils/dbwire.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue