Some more bug fixes for dbwire

This commit is contained in:
Miroslav Štampar 2026-07-12 12:01:51 +02:00
parent a61e35980a
commit 9d08b1dc03
4 changed files with 83 additions and 21 deletions

View file

@ -36,10 +36,11 @@ _CLIENT_PLUGIN_AUTH = 0x00080000
_MAX_PACKET = 0x1000000
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) payload, to bound a hostile/corrupt stream
_BINARY_CHARSET = 63 # collation id 63 == 'binary'
# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/BIT/GEOMETRY family).
# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/GEOMETRY family).
# Numeric & temporal columns ALSO report charset 63 in the text protocol, but carry their ASCII text form -
# they must be decoded, not returned as bytes (else -d hexifies e.g. the int 12345 to '3132333435').
_BINARY_TYPES = frozenset((15, 16, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,BIT,*BLOB,VAR_STRING,STRING,GEOMETRY
_BINARY_TYPES = frozenset((15, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,*BLOB,VAR_STRING,STRING,GEOMETRY
_TYPE_BIT = 16 # BIT reports charset 63 but is decoded to a big-endian integer (matches SQLAlchemy/mysql-connector)
def _xor(a, b):
if str is bytes: # Python 2
@ -117,6 +118,12 @@ def _err_message(payload):
off = 9
return payload[off:].decode("utf-8", "replace")
def _bit_int(value):
n = 0 # BIT arrives as a big-endian byte string
for b in bytearray(value):
n = (n << 8) | b
return n
def _scramble_native(password, salt):
if not password:
return b""
@ -232,6 +239,8 @@ class Connection(object):
value, off = _lenc_str(payload, off)
if value is None:
row.append(None)
elif description[i][1] == _TYPE_BIT:
row.append(str(_bit_int(value))) # big-endian integer, e.g. b'\x2a' -> '42'
elif binary[i]:
row.append(value) # keep binary/BLOB columns as raw bytes (sqlmap hex-encodes them)
else:
@ -327,10 +336,15 @@ def connect(host=None, port=3306, user=None, password=None, database=None, conne
raise OperationalError("handshake failed (%s)" % ex)
connection = Connection(sock)
try:
connection._query("SET autocommit=1") # so DML persists even if the server default is autocommit=0
except Exception:
pass
# SET NAMES: reset collation_connection to the server's default (the fixed handshake collation 45 =
# utf8mb4_general_ci otherwise clashes with MySQL 8's utf8mb4_0900_ai_ci columns -> 'illegal mix of
# collations' 1271 in a UNION/CONCAT); results stay utf8mb4 so the utf-8 decode is unchanged. autocommit=1
# so DML persists even if the server default is autocommit=0. Both best-effort (one-time, at connect).
for setup in ("SET NAMES utf8mb4", "SET autocommit=1"):
try:
connection._query(setup)
except Exception:
pass
return connection
def _safe_close(sock):

View file

@ -32,6 +32,28 @@ from extra.dbwire import ProgrammingError
_PROTOCOL_VERSION = 196608 # 3.0
_MAX_MESSAGE_LENGTH = 0x40000000 # 1 GB - guard against a hostile/corrupt length triggering an unbounded read
_OID_BYTEA = 17 # bytea arrives as the server's text form; decode to bytes so it hexes like the native driver
def _decode_bytea(raw):
# PG text output: modern 'hex' = b'\\x<hexdigits>'; legacy 'escape' = octal \ooo + literal bytes
if raw[:2] == b"\\x":
try:
return binascii.unhexlify(raw[2:])
except (binascii.Error, ValueError):
return raw.decode("utf-8", "replace")
src, out, i, n = bytearray(raw), bytearray(), 0, len(raw)
while i < n:
if src[i] == 0x5c and i + 1 < n: # backslash
nxt = src[i + 1]
if nxt == 0x5c:
out.append(0x5c); i += 2
elif 0x30 <= nxt <= 0x37 and i + 3 < n: # \ooo octal
out.append(((nxt - 48) << 6) | ((src[i + 2] - 48) << 3) | (src[i + 3] - 48)); i += 4
else:
out.append(nxt); i += 2
else:
out.append(src[i]); i += 1
return bytes(out)
# SQLSTATE class (first 2 chars) -> DB-API exception, so callers can distinguish (mirrors psycopg2)
_SQLSTATE_CLASS = {
@ -114,6 +136,7 @@ class Cursor(object):
class Connection(object):
def __init__(self, sock):
self._sock = sock
self._txn_status = b"I" # last ReadyForQuery transaction status: I(dle) / T(ransaction) / E(rror)
def cursor(self):
return Cursor(self)
@ -134,7 +157,19 @@ class Connection(object):
except Exception:
pass
def _clear_aborted(self):
# a prior statement left an aborted transaction block ('E'): every further statement errors with
# 25P02 until it is rolled back. Clear it so the reused connection recovers (psycopg2 rollback semantics).
_send(self._sock, b"Q", b"ROLLBACK\x00")
while True:
mtype, payload = _read_message(self._sock)
if mtype == b"Z":
self._txn_status = payload[:1] or b"I"
break
def _simple_query(self, query):
if self._txn_status == b"E":
self._clear_aborted()
_send(self._sock, b"Q", query.encode("utf-8") + b"\x00")
description, rows, rowcount, error = None, [], -1, None
@ -154,7 +189,7 @@ class Connection(object):
elif mtype == b"D": # DataRow
(count,) = struct.unpack("!H", payload[:2])
off, row = 2, []
for _ in range(count):
for col in range(count):
(vlen,) = struct.unpack("!i", payload[off:off + 4])
off += 4
if vlen == -1:
@ -162,8 +197,15 @@ class Connection(object):
else:
if off + vlen > len(payload):
raise InterfaceError("truncated DataRow")
row.append(payload[off:off + vlen].decode("utf-8", "replace"))
raw = payload[off:off + vlen]
off += vlen
if description and col < len(description) and description[col][1] == _OID_BYTEA:
row.append(_decode_bytea(raw)) # bytes so sqlmap hex-encodes it (like the native driver)
else:
try:
row.append(raw.decode("utf-8"))
except UnicodeDecodeError:
row.append(raw) # non-UTF-8 (e.g. a SQL_ASCII db): keep bytes (hex-encoded), not lossy U+FFFD
rows.append(tuple(row))
elif mtype == b"C": # CommandComplete ("SELECT 3", "INSERT 0 1", ...)
tag = payload[:-1].decode("utf-8", "replace").split()
@ -173,7 +215,8 @@ class Connection(object):
_send(self._sock, b"f", b"COPY FROM STDIN is not supported\x00") # CopyFail
elif mtype == b"E": # ErrorResponse
error = _error_message(payload)
elif mtype == b"Z": # ReadyForQuery (end of response)
elif mtype == b"Z": # ReadyForQuery (end of response); payload byte = transaction status
self._txn_status = payload[:1] or b"I"
break
# ParameterStatus(S)/NoticeResponse(N)/EmptyQueryResponse(I)/CopyData(d)/CopyDone(c)/... ignored
except (struct.error, IndexError, ValueError) as ex:

View file

@ -131,7 +131,10 @@ def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire")
header += struct.pack("<I", 0) # client prog version
header += struct.pack("<I", 0) # client PID
header += struct.pack("<I", 0) # connection id
header += struct.pack("<BBBB", 0, 0, 0, 0) # option flags 1/2, type flags, option flags 3
# OptionFlags2 fODBC (0x02): make the server prepare the session like ODBC/FreeTDS - SET ANSI_DEFAULTS ON
# (ANSI_NULLS/WARNINGS/PADDING, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER) + TEXTSIZE 2147483647 (else the
# default 4096 silently truncates varchar(max)/text/image dumps) + IMPLICIT_TRANSACTIONS OFF + ROWCOUNT OFF
header += struct.pack("<BBBB", 0, 0x02, 0, 0) # option flags 1, option flags 2 (fODBC), type flags, option flags 3
header += struct.pack("<i", 0) # client time zone
header += struct.pack("<I", 0) # client LCID
@ -157,7 +160,13 @@ def _decode_money(raw):
else: # 8 bytes: signed high dword, unsigned low dword
hi, lo = struct.unpack("<iI", raw)
v = (hi << 32) | lo
return "%.4f" % (v / 10000.0)
s = "%05d" % abs(v) # scale 4; format from the integer (int64 money exceeds float's ~15-digit precision)
return ("-" if v < 0 else "") + s[:-4] + "." + s[-4:]
def _decode_smalldatetime(raw):
import datetime
days, mins = struct.unpack("<HH", raw) # 2-byte days since 1900-01-01 + 2-byte minutes
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins))
def _decode_numeric(raw, scale):
sign = bytearray(raw)[0] # 1 == positive, 0 == negative; magnitude is little-endian
@ -276,6 +285,8 @@ def _decode_variant(body):
return _decode_money(val)
if base == 0x3d:
return _decode_datetime(val)
if base == 0x3a:
return _decode_smalldatetime(val)
if base == 0x24:
return _decode_guid(val)
if base in (0x6a, 0x6c): # decimal/numeric: metadata = precision, scale
@ -381,10 +392,8 @@ def _decode_value(col, data, off):
return _decode_money(data[off:off + 8]), off + 8
if t == 0x3d: # DATETIME (8 bytes)
return _decode_datetime(data[off:off + 8]), off + 8
if t == 0x3a: # DATETIM4 / smalldatetime (2-byte days since 1900 + 2-byte minutes)
import datetime
days, mins = struct.unpack("<HH", data[off:off + 4])
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins)), off + 4
if t == 0x3a: # DATETIM4 / smalldatetime (4 bytes)
return _decode_smalldatetime(data[off:off + 4]), off + 4
# variable-length with a length prefix
if t in (0xa7, 0xaf, 0xe7, 0xef, 0xa5, 0xad):
@ -448,11 +457,7 @@ def _decode_value(col, data, off):
if t == 0x24: # GUID
return _decode_guid(raw), off
if t == 0x6f: # DATETIMN (n=8 datetime, n=4 smalldatetime)
if n == 8:
return _decode_datetime(raw), off
import datetime
days, mins = struct.unpack("<HH", raw)
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins)), off
return (_decode_datetime(raw) if n == 8 else _decode_smalldatetime(raw)), off
if t in (0x28, 0x29, 0x2a, 0x2b): # DATE / TIME / DATETIME2 / DATETIMEOFFSET
return _decode_temporal(t, col.scale, raw), off
# unknown layout: return the raw hex as a last resort (never desyncs)

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.87"
VERSION = "1.10.7.88"
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)