mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
442 lines
16 KiB
Python
442 lines
16 KiB
Python
#!/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)
|