diff --git a/data/xml/queries.xml b/data/xml/queries.xml
index b619a7a88..28f0526a6 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 cadafcf00..cfd37dceb 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 000000000..ae6f95f5e
--- /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 49a75f86b..2387a4772 100644
--- a/lib/core/dicts.py
+++ b/lib/core/dicts.py
@@ -272,6 +272,7 @@ DBWIRE_MODULES = {
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 75754d335..6e30f5b2a 100644
--- a/lib/core/settings.py
+++ b/lib/core/settings.py
@@ -20,7 +20,7 @@ from lib.core.enums import OS
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 cc17cd8aa..cd869ec20 100644
--- a/plugins/dbms/cubrid/connector.py
+++ b/plugins/dbms/cubrid/connector.py
@@ -33,7 +33,7 @@ class Connector(GenericConnector):
# 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()