diff --git a/data/xml/queries.xml b/data/xml/queries.xml index dfaabd38c8..b619a7a882 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -222,7 +222,7 @@ - + diff --git a/extra/dbwire/__init__.py b/extra/dbwire/__init__.py new file mode 100644 index 0000000000..e48a84e4d5 --- /dev/null +++ b/extra/dbwire/__init__.py @@ -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 diff --git a/extra/dbwire/clickhouse.py b/extra/dbwire/clickhouse.py new file mode 100644 index 0000000000..b6a9cae588 --- /dev/null +++ b/extra/dbwire/clickhouse.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +""" +Minimal pure-python ClickHouse client over its native HTTP interface (stdlib only, no clickhouse_connect). + +ClickHouse exposes an HTTP endpoint that runs a query in the request body and streams the result back in a +chosen format; we use TabSeparatedWithNames (first line = column names, then tab-separated rows with +backslash escaping and \\N for NULL). Covers ClickHouse and its HTTP-compatible forks. +""" + +import base64 +import socket + +try: + from urllib.request import Request, urlopen # Python 3 + from urllib.error import HTTPError, URLError +except ImportError: + from urllib2 import Request, urlopen, HTTPError, URLError # Python 2 + +from extra.dbwire import OperationalError +from extra.dbwire import ProgrammingError + +# TabSeparated backslash escapes -> the literal byte they denote +_ESCAPE = {ord("t"): 9, ord("n"): 10, ord("r"): 13, ord("0"): 0, ord("b"): 8, + ord("f"): 12, ord("a"): 7, ord("v"): 11, ord("\\"): 92, ord("'"): 39} + +def _unescape(value): + # value: the raw bytes of one TSV field -> None (\N) or the unescaped bytes. Operates on bytes because a + # String/FixedString column can hold arbitrary non-UTF-8 data, which a whole-body utf-8 decode would destroy. + if value == b"\\N": + return None + if b"\\" not in value: + return value + src, out, i, n = bytearray(value), bytearray(), 0, len(value) + while i < n: + c = src[i] + if c == 0x5c and i + 1 < n: # backslash + out.append(_ESCAPE.get(src[i + 1], src[i + 1])); i += 2 + else: + out.append(c); i += 1 + return bytes(out) + +def _decode_cell(value): + # keep text as str; hand back raw bytes only when a value is not valid UTF-8 (sqlmap then hex-encodes it) + if value is None: + return None + try: + return value.decode("utf-8") + except UnicodeDecodeError: + return value + +class Cursor(object): + def __init__(self, connection): + self.connection = connection + self.description = None + self.rowcount = -1 + self._rows = [] + self._pos = 0 + + def execute(self, query, params=None): + if params is not None: + raise ProgrammingError("parameter binding is not supported; pass a fully-formed query string") + self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0 + self.description, self._rows = self.connection._query(query) + self.rowcount = len(self._rows) + return self + + def fetchall(self): + retVal = self._rows[self._pos:] + self._pos = len(self._rows) + return retVal + + def fetchone(self): + if self._pos >= len(self._rows): + return None + retVal = self._rows[self._pos] + self._pos += 1 + return retVal + + def close(self): + self._rows = [] + +class Connection(object): + def __init__(self, host, port, user, password, database, timeout): + self._url = "http://%s:%d/?database=%s&default_format=TabSeparatedWithNames" % (host, port, database or "default") + self._headers = {} + if user or password: + token = base64.b64encode(("%s:%s" % (user or "", password or "")).encode("utf-8")).decode("ascii") + self._headers["Authorization"] = "Basic %s" % token + self._timeout = timeout + + def cursor(self): + return Cursor(self) + + def commit(self): + pass # ClickHouse statements are executed immediately (no client-side transaction) + + def rollback(self): + pass + + def close(self): + pass # HTTP is stateless + + def _query(self, query): + req = Request(self._url, data=query.encode("utf-8"), headers=self._headers) + try: + body = urlopen(req, timeout=self._timeout).read() # bytes: column data may be non-UTF-8 + except HTTPError as ex: + raise ProgrammingError("(remote) %s" % ex.read().decode("utf-8", "replace").strip()) + except URLError as ex: + raise OperationalError("(remote) %s" % ex) + except (socket.timeout, socket.error) as ex: + raise OperationalError("(remote) %s" % ex) + + if not body: + return None, [] + lines = body.split(b"\n") + if lines and lines[-1] == b"": + lines.pop() + if not lines: + return None, [] + description = [(name, None, None, None, None, None, None) for name in (_decode_cell(_unescape(_)) for _ in lines[0].split(b"\t"))] + rows = [tuple(_decode_cell(_unescape(_)) for _ in line.split(b"\t")) for line in lines[1:]] + return description, rows + +def connect(host=None, port=8123, user=None, password=None, database=None, connect_timeout=None, **kwargs): + connection = Connection(host or "localhost", int(port or 8123), user, password, database, connect_timeout) + try: + connection._query("SELECT 1") # verify connectivity/credentials up front + except ProgrammingError: + raise + except Exception as ex: + raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) + return connection diff --git a/extra/dbwire/monetdb.py b/extra/dbwire/monetdb.py new file mode 100644 index 0000000000..37f6db5c02 --- /dev/null +++ b/extra/dbwire/monetdb.py @@ -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("> 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("= 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: " # " + 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 diff --git a/extra/dbwire/mysql.py b/extra/dbwire/mysql.py new file mode 100644 index 0000000000..4f9b672622 --- /dev/null +++ b/extra/dbwire/mysql.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +""" +Minimal pure-python MySQL client/server protocol client (stdlib only). + +Covers the whole MySQL-wire family (MySQL, MariaDB, TiDB, Aurora-MySQL, Percona, ...). Auth: +mysql_native_password (full), plus caching_sha2_password fast path; caching_sha2 *full* auth over a +plaintext connection needs RSA (not in the stdlib), so that case raises a clean NotSupportedError - use a +mysql_native_password account (as MariaDB/TiDB default to) for the dependency-free path. +""" + +import hashlib +import socket +import struct + +from extra.dbwire import DatabaseError +from extra.dbwire import InterfaceError +from extra.dbwire import NotSupportedError +from extra.dbwire import OperationalError +from extra.dbwire import ProgrammingError + +# capability flags +_CLIENT_LONG_PASSWORD = 0x00000001 +_CLIENT_LONG_FLAG = 0x00000004 +_CLIENT_CONNECT_WITH_DB = 0x00000008 +_CLIENT_PROTOCOL_41 = 0x00000200 +_CLIENT_TRANSACTIONS = 0x00002000 +_CLIENT_SECURE_CONNECTION = 0x00008000 +_CLIENT_PLUGIN_AUTH = 0x00080000 + +_MAX_PACKET = 0x1000000 +_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) payload, to bound a hostile/corrupt stream +_BINARY_CHARSET = 63 # collation id 63 == 'binary' +# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/BIT/GEOMETRY family). +# Numeric & temporal columns ALSO report charset 63 in the text protocol, but carry their ASCII text form - +# they must be decoded, not returned as bytes (else -d hexifies e.g. the int 12345 to '3132333435'). +_BINARY_TYPES = frozenset((15, 16, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,BIT,*BLOB,VAR_STRING,STRING,GEOMETRY + +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(" _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(" 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(" 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: \x00 + 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(" 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) diff --git a/extra/dbwire/presto.py b/extra/dbwire/presto.py new file mode 100644 index 0000000000..7deede433a --- /dev/null +++ b/extra/dbwire/presto.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) +See the file 'LICENSE' for copying permission +""" + +""" +Minimal pure-python Presto/Trino client over its native HTTP/REST interface (stdlib only, no +presto-python-client). A query is POSTed to /v1/statement; the server returns JSON pages carrying +'columns'/'data' and a 'nextUri' to poll until the statement finishes. Both X-Presto-* and X-Trino-* +headers are sent so the same client works against Presto and Trino. +""" + +import base64 +import json +import socket + +try: + from urllib.request import Request, urlopen # Python 3 + from urllib.error import HTTPError, URLError +except ImportError: + from urllib2 import Request, urlopen, HTTPError, URLError # Python 2 + +from extra.dbwire import InterfaceError +from extra.dbwire import NotSupportedError +from extra.dbwire import OperationalError +from extra.dbwire import ProgrammingError + +def _convert(value, coltype): + # normalize Presto/Trino JSON cells for sqlmap: VARBINARY arrives base64-encoded (decode to bytes so + # direct()'s binary handling hex-encodes it), ARRAY/MAP/ROW arrive as JSON structures (serialize to text) + if value is None: + return value + if coltype.startswith("varbinary"): + try: + return base64.b64decode(value) + except Exception: + return value + if isinstance(value, (list, dict)): + return json.dumps(value) + return value + +class Cursor(object): + def __init__(self, connection): + self.connection = connection + self.description = None + self.rowcount = -1 + self._rows = [] + self._pos = 0 + + def execute(self, query, params=None): + if params is not None: + raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string") + self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0 + self.description, self._rows = self.connection._query(query) + self.rowcount = len(self._rows) + return self + + def fetchall(self): + retVal = self._rows[self._pos:] + self._pos = len(self._rows) + return retVal + + def fetchone(self): + if self._pos >= len(self._rows): + return None + retVal = self._rows[self._pos] + self._pos += 1 + return retVal + + def close(self): + self._rows = [] + +class Connection(object): + def __init__(self, host, port, user, password, catalog, schema, timeout): + self._statement_url = "http://%s:%d/v1/statement" % (host, port) + self._timeout = timeout + self._headers = {"Content-Type": "text/plain"} + for prefix in ("X-Presto-", "X-Trino-"): + self._headers[prefix + "User"] = user or "sqlmap" + self._headers[prefix + "Source"] = "dbwire" + # only send Catalog/Schema when supplied: a Schema without a Catalog makes Trino reject every + # request ("Schema is set but catalog is not"), so never force a "default" schema + if catalog: + self._headers[prefix + "Catalog"] = catalog + if schema: + self._headers[prefix + "Schema"] = schema + if password: + token = base64.b64encode(("%s:%s" % (user or "", password)).encode("utf-8")).decode("ascii") + self._headers["Authorization"] = "Basic %s" % token + + def cursor(self): + return Cursor(self) + + def commit(self): + pass + + def rollback(self): + pass + + def close(self): + pass # HTTP is stateless + + def _request(self, url, data=None): + req = Request(url, data=data.encode("utf-8") if data is not None else None, headers=self._headers) + try: + body = urlopen(req, timeout=self._timeout).read().decode("utf-8", "replace") + except HTTPError as ex: + raise ProgrammingError("(remote) HTTP %s: %s" % (ex.code, ex.read().decode("utf-8", "replace")[:200])) + except URLError as ex: + raise OperationalError("(remote) %s" % ex) + except (socket.timeout, socket.error) as ex: + raise OperationalError("(remote) %s" % ex) + try: + return json.loads(body) + except ValueError as ex: + raise InterfaceError("malformed server response: %s" % ex) + + def _query(self, query): + page = self._request(self._statement_url, data=query) + columns, rows, types = None, [], [] + while True: + if page.get("error"): + message = page["error"].get("message", "unknown error") + raise ProgrammingError("(remote) %s" % message) + if page.get("columns") and columns is None: + columns = [(c.get("name"), c.get("type"), None, None, None, None, None) for c in page["columns"]] + types = [(c.get("type") or "") for c in page["columns"]] + for row in page.get("data") or []: + rows.append(tuple(_convert(v, types[i] if i < len(types) else "") for i, v in enumerate(row))) + next_uri = page.get("nextUri") + if not next_uri: + break + page = self._request(next_uri) + return columns, rows + +def connect(host=None, port=8080, user=None, password=None, database=None, connect_timeout=None, schema=None, **kwargs): + connection = Connection(host or "localhost", int(port or 8080), user, password, database, schema, connect_timeout) + try: + connection._query("SELECT 1") # verify connectivity/credentials + except ProgrammingError: + raise + except Exception as ex: + raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex)) + return connection diff --git a/extra/dbwire/tds.py b/extra/dbwire/tds.py new file mode 100644 index 0000000000..28ec16d041 --- /dev/null +++ b/extra/dbwire/tds.py @@ -0,0 +1,608 @@ +#!/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("= 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("= 0 else ("-", -offset) + s += " %s%02d:%02d" % (sign, mins // 60, mins % 60) + return s + +# SQL Server COLLATION -> Python codec. The 5-byte collation is a little-endian uint32 (low 20 bits = LCID) +# plus a 1-byte sort id: a non-zero sort id fixes the code page, else the LCID does. Only single-byte / DBCS +# code pages need a codec (NVARCHAR is UTF-16, handled separately). Derived from pytds; default cp1252 (the +# stock SQL_Latin1_General code page - NOT latin-1, whose 0x80-0x9F differ, corrupting e.g. the euro sign). +_LCID_CP = { + 0x405: "cp1250", 0x40e: "cp1250", 0x415: "cp1250", 0x418: "cp1250", 0x41a: "cp1250", 0x41b: "cp1250", + 0x41c: "cp1250", 0x424: "cp1250", 0x402: "cp1251", 0x419: "cp1251", 0x422: "cp1251", 0x423: "cp1251", + 0x42f: "cp1251", 0x408: "cp1253", 0x41f: "cp1254", 0x42c: "cp1254", 0x443: "cp1254", 0x40d: "cp1255", + 0x401: "cp1256", 0x420: "cp1256", 0x429: "cp1256", 0x425: "cp1257", 0x426: "cp1257", 0x427: "cp1257", + 0x42a: "cp1258", 0x41e: "cp874", 0x411: "cp932", 0x804: "cp936", 0x1004: "cp936", 0x412: "cp949", + 0x404: "cp950", 0xc04: "cp950", 0x1404: "cp950", +} + +def _sortid_cp(sid): + if 30 <= sid <= 34: + return "cp437" + if 40 <= sid <= 44 or sid == 49 or 55 <= sid <= 61: + return "cp850" + if sid in (51, 52, 53, 54) or 183 <= sid <= 186: + return "cp1252" + if 80 <= sid <= 96: + return "cp1250" + if 104 <= sid <= 108: + return "cp1251" + if 112 <= sid <= 124: + return "cp1253" + if 128 <= sid <= 130: + return "cp1254" + if 136 <= sid <= 138: + return "cp1255" + if 144 <= sid <= 146: + return "cp1256" + if 152 <= sid <= 160: + return "cp1257" + return None + +def _collation_codec(collation): + if not collation or len(collation) < 5: + return "cp1252" + lump = struct.unpack(" raw bytes + if base in (0xa7, 0xaf): # (var)char: metadata = 5-byte collation + 2-byte max length + return val.decode(_collation_codec(meta[:5]), "replace") + if base == 0x28: + return _decode_temporal(base, 0, val) + if base in (0x29, 0x2a, 0x2b): # metadata = scale + return _decode_temporal(base, bytearray(meta)[0], val) + return "".join("%02x" % x for x in bytearray(val)) # unknown base type -> hex (never desyncs) + +class _Column(object): + __slots__ = ("name", "type", "size", "scale", "binary", "collation") + +def _parse_type_info(data, off): + col = _Column() + col.type = _u8(data, off); off += 1 + col.size, col.scale, col.binary, col.collation = 0, 0, False, None + t = col.type + if t in (0x30, 0x32, 0x34, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x7a, 0x7f, 0x1f): + pass # fixed-length types, size implied by type + elif t in (0x26, 0x68, 0x6d, 0x6e, 0x6f, 0x24): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID + col.size = _u8(data, off); off += 1 + elif t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN/NUMERICN + legacy DECIMAL/NUMERIC (size, precision, scale) + col.size = _u8(data, off); off += 1 + off += 1 # precision + col.scale = _u8(data, off); off += 1 + elif t in (0xa7, 0xaf, 0xe7, 0xef): # (BIG)VARCHAR/CHAR, N(VAR)CHAR + col.size = struct.unpack(" raw bytes + if t in (0xe7, 0xef): + return raw.decode("utf-16-le", "replace"), off + return raw.decode(_collation_codec(col.collation), "replace"), off # (var)char: the collation's code page + + if t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE: 1-byte textptr len (0 = NULL) then textptr+timestamp then 4-byte len + ptr_len = _u8(data, off); off += 1 + if ptr_len == 0: + return None, off + off += ptr_len + 8 + (n,) = struct.unpack(" sqlmap hex-encodes them + return _read_plp(data, off) + + if t == 0x62: # SQL_VARIANT: 4-byte total length (0 = NULL) then a self-describing value body + (total,) = struct.unpack("= 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(" 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", diff --git a/lib/core/settings.py b/lib/core/settings.py index fba21d81b4..814a1a1c49 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.84" +VERSION = "1.10.7.87" 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) diff --git a/lib/utils/dbwire.py b/lib/utils/dbwire.py new file mode 100644 index 0000000000..ce81a0fb20 --- /dev/null +++ b/lib/utils/dbwire.py @@ -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()