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