diff --git a/extra/dbwire/cubrid.py b/extra/dbwire/cubrid.py index 750d7578b..4aba46131 100644 --- a/extra/dbwire/cubrid.py +++ b/extra/dbwire/cubrid.py @@ -132,6 +132,10 @@ class _Reader(object): v = struct.unpack_from(">d", self._buf, self._off)[0]; self._off += 8; return v def raw(self, n): + # a slice past the end returns short data *silently*, which would hand a truncated value to the + # caller (the fixed-width readers above raise struct.error instead) + if n < 0 or n > self.remaining(): + raise InterfaceError("CAS response too short (wanted %d of %d bytes)" % (n, self.remaining())) v = self._buf[self._off:self._off + n]; self._off += n; return bytes(v) def skip(self, n): @@ -371,9 +375,15 @@ class Connection(object): reader.byte() # is_updatable columns = self._parse_columns(reader, reader.int()) + # args: handle, flag, max_col_size, max_row, binds, fetch_flag, auto_commit, forward_only_cursor, + # cache_time, query_timeout. auto_commit makes the CAS worker commit the statement and end the + # transaction - without it DML is rolled back when the connection drops, whatever _open() asked + # SET_DB_PARAMETER for. It must stay off for a SELECT: ending the transaction there invalidates the + # request handle the paged _fetch_remaining() still reads from. + select = stmt_type == _STMT_SELECT 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)) + .arg_null().arg_byte(1 if select else 0) + .arg_byte(0 if select else 1).arg_byte(1).arg_cache_time().arg_int(0)) reader = self._call(exec_writer) total = reader.int() diff --git a/extra/dbwire/firebird.py b/extra/dbwire/firebird.py index a7a56da2e..218fbf46a 100644 --- a/extra/dbwire/firebird.py +++ b/extra/dbwire/firebird.py @@ -142,6 +142,8 @@ _GDS_INTEGRITY = frozenset((335544838, 335544879, 335544880, 335544466, 33554466 _GDS_DATA = frozenset((335544321,)) _GDS_WARNING = 335544434 +_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a wire-supplied length, to bound a hostile/corrupt stream + # SRP-6a group used by Firebird (fixed 1024-bit prime, generator 2) _SRP_N = int("E67D2E994B2F900C3F41F08F5BB2627ED0D49EE1FE767A52EFCD565CD6E768812C3E1E9CE8F0A8BEA6CB13CD29DDE" "BF7A96D4A93B55D488DF099A15C89DCB0640738EB2CBDD9A8F7BAB561AB1B0DC1C6CDABF303264A08D1BCA932D1F" @@ -306,6 +308,10 @@ class _Wire(object): return buf def recv(self, n, align=False): + # every length here comes off the wire (response buffers, status strings, per-value lengths): a + # negative one would silently return short data, a huge one would read until memory ran out + if n < 0 or n > _MAX_MESSAGE_LENGTH: + raise InterfaceError("invalid Firebird length (%d)" % n) total = n + ((4 - n % 4) % 4) if align else n data = self._recv_raw(total) if self._rc: diff --git a/extra/dbwire/monetdb.py b/extra/dbwire/monetdb.py index 90dd4451f..b51b031a3 100644 --- a/extra/dbwire/monetdb.py +++ b/extra/dbwire/monetdb.py @@ -27,6 +27,7 @@ from extra.dbwire import keepalive from extra.dbwire import ProgrammingError _MAX_BLOCK = 0xffff >> 1 +_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) response, to bound a hostile/corrupt stream def _recvn(sock, n): buf = b"" @@ -41,14 +42,19 @@ def _recvn(sock, n): return buf def _getblock(sock): - out = b"" + # the block length is a 15-bit field, so bounding IT is pointless - a peer that never sets the last-flag + # simply streams blocks forever. Bound the accumulated response instead (as the other wire modules do). + chunks, total = [], 0 while True: (header,) = struct.unpack("> 1, header & 1 - out += _recvn(sock, length) + total += length + if total > _MAX_MESSAGE_LENGTH: + raise InterfaceError("backend message too large (%d bytes)" % total) + chunks.append(_recvn(sock, length)) if last: break - return out.decode("utf-8", "replace") + return b"".join(chunks).decode("utf-8", "replace") def _putblock(sock, text): data = text.encode("utf-8") diff --git a/extra/dbwire/postgres.py b/extra/dbwire/postgres.py index 8402095ab..db576122e 100644 --- a/extra/dbwire/postgres.py +++ b/extra/dbwire/postgres.py @@ -321,9 +321,9 @@ def connect(host=None, port=5432, user=None, password=None, database=None, conne for key, value in (("user", user or ""), ("database", database or user or ""), ("client_encoding", "UTF8")): params += key.encode("ascii") + b"\x00" + ("%s" % value).encode("utf-8") + b"\x00" params += b"\x00" - _send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params) try: + _send(sock, b"", struct.pack("!I", _PROTOCOL_VERSION) + params) # StartupMessage _authenticate(sock, user, password) while True: # drain until ReadyForQuery (ParameterStatus/BackendKeyData/NoticeResponse) mtype, payload = _read_message(sock) diff --git a/lib/core/settings.py b/lib/core/settings.py index 6b3bf99b1..fd0ba8019 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.8.29" +VERSION = "1.10.8.30" 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/tests/test_dbwire.py b/tests/test_dbwire.py index 57dd32a88..2d3b68370 100644 --- a/tests/test_dbwire.py +++ b/tests/test_dbwire.py @@ -31,7 +31,10 @@ bootstrap() import extra.dbwire as dbwire from extra.dbwire import connection_lost +from extra.dbwire import cubrid as _cubrid +from extra.dbwire import firebird as _firebird from extra.dbwire import http_origin +from extra.dbwire import monetdb as _monetdb from extra.dbwire import mysql as _mysql from extra.dbwire import postgres as _postgres from extra.dbwire import presto as _presto @@ -314,6 +317,76 @@ class TrinoSessionStateTest(unittest.TestCase): self.assertNotIn("X-Presto-Schema", c._headers) +class CubridAutoCommitTest(unittest.TestCase): + """CUBRID has no server-side autocommit switch that outlives a statement: the EXECUTE request carries + its own auto_commit byte, and a statement sent with it clear is rolled back when the CAS worker goes + away. It must stay clear for a SELECT, whose request handle the paged fetch still reads from.""" + + def _auto_commit_byte(self, stmt_type): + connection = object.__new__(_cubrid.Connection) + connection._protocol_version = 8 + connection._sock = None + sent = [] + + def _call(writer): + sent.append(writer.payload()) + # EXECUTE response: total(4) cache_reusable(1) result_count(4) includes_column_info(1) shard_id(4) + return _cubrid._Reader(struct.pack(">iBiBi", 0, 0, 0, 0, 0)) + + connection._call = _call + prepare = _cubrid._Reader(struct.pack(">iBiBi", 0, stmt_type, 0, 0, 0)) + connection._execute(1, prepare) + + args, off = [], 1 # skip the function code, then walk [len(4)][value] args + payload = sent[0] + while off < len(payload): + (length,) = struct.unpack(">i", payload[off:off + 4]) + args.append(payload[off + 4:off + 4 + length]) + off += 4 + length + return bytearray(args[6])[0] # handle, flag, max_col_size, max_row, binds, fetch, auto_commit + + def test_dml_is_committed_by_the_execute_request(self): + self.assertEqual(self._auto_commit_byte(_cubrid._STMT_SELECT + 1), 1) + + def test_select_does_not_end_the_transaction(self): + self.assertEqual(self._auto_commit_byte(_cubrid._STMT_SELECT), 0) + + +class BoundedReadTest(unittest.TestCase): + """A length taken off the wire is attacker/corruption controlled: unchecked, it either reads until + memory runs out or (on a short buffer) hands back silently truncated data.""" + + def test_cubrid_short_response_is_not_silently_truncated(self): + reader = _cubrid._Reader(b"AB") + self.assertRaises(dbwire.InterfaceError, reader.raw, 8) + self.assertRaises(dbwire.InterfaceError, reader.raw, -1) + + def test_firebird_rejects_an_out_of_range_length(self): + wire = _firebird._Wire(FakeSocket()) + self.assertRaises(dbwire.InterfaceError, wire.recv, -1) + self.assertRaises(dbwire.InterfaceError, wire.recv, _firebird._MAX_MESSAGE_LENGTH + 1) + + def test_monetdb_unterminated_block_stream_is_bounded(self): + """The MAPI block length is a 15-bit field, so only the accumulated response can be bounded.""" + + block = struct.pack(" never ends + sock = FakeSocket(block * 32) + original = sock.recv + + def recv(count): + if not sock.inbound: + sock.feed(block * 32) + return original(count) + + sock.recv = recv + saved = _monetdb._MAX_MESSAGE_LENGTH + try: + _monetdb._MAX_MESSAGE_LENGTH = 100000 + self.assertRaises(dbwire.InterfaceError, _monetdb._getblock, sock) + finally: + _monetdb._MAX_MESSAGE_LENGTH = saved + + class HelperTest(unittest.TestCase): def test_socket_failure_maps_into_the_dbapi_hierarchy(self): """Callers of a PEP 249 driver only catch Error and its subclasses."""