Adding CUBRID to dbwire

This commit is contained in:
Miroslav Štampar 2026-07-12 13:37:04 +02:00
parent dad2f02f28
commit 59933891e0
6 changed files with 451 additions and 5 deletions

View file

@ -1509,7 +1509,7 @@
<current_db query="CURRENT_USER"/>
<hostname/>
<table_comment query="SELECT comment FROM db_class WHERE owner_name='%s' AND class_name='%s'"/>
<column_comment query="SELECT db_attribute.comment FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE owner_name='%s' AND db_class.class_name='%s' AND attr_name='%s'"/>
<column_comment query="SELECT db_attribute.comment FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.owner_name='%s' AND db_class.class_name='%s' AND attr_name='%s'"/>
<is_dba query="CURRENT_USER='DBA'"/>
<check_udf query="(SELECT meth_name FROM db_method WHERE meth_name='%s' LIMIT 0,1)='%s'"/>
<users>
@ -1532,8 +1532,8 @@
<blind query="SELECT class_name FROM db_class WHERE owner_name='%s' LIMIT %d,1" count="SELECT COUNT(class_name) FROM db_class WHERE owner_name='%s'"/>
</tables>
<columns>
<inband query="SELECT attr_name,data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" condition="attr_name"/>
<blind query="SELECT attr_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" query2="SELECT data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" count="SELECT COUNT(attr_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND owner_name='%s'" condition="attr_name"/>
<inband query="SELECT attr_name,data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" condition="attr_name"/>
<blind query="SELECT attr_name FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" query2="SELECT data_type FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" count="SELECT COUNT(attr_name) FROM db_attribute JOIN db_class ON db_attribute.class_name=db_class.class_name WHERE db_class.class_name='%s' AND db_class.owner_name='%s'" condition="attr_name"/>
</columns>
<dump_table>
<inband query="SELECT %s FROM %s.%s"/>

View file

@ -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.

442
extra/dbwire/cubrid.py Normal file
View file

@ -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)

View file

@ -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/

View file

@ -20,7 +20,7 @@ from lib.core.enums import OS
from thirdparty import six
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.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)

View file

@ -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()