#!/usr/bin/env python """ Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) See the file 'LICENSE' for copying permission """ """ Minimal pure-python TDS (Tabular Data Stream) client for Microsoft SQL Server / Sybase (stdlib only). Cleartext login only (TDS pre-login encryption negotiated to NOT_SUP); a server that forces encryption would need TLS-in-TDS which is out of scope for the dependency-free client. Implements PRELOGIN, LOGIN7, SQL batch, and decoding of the common column types (int/bit/float/money/decimal, (n)char/(n)varchar and their MAX/PLP forms, binary, guid, datetime family) to text (binary columns are returned as raw bytes so sqlmap hex-encodes them). """ import socket import struct from extra.dbwire import DatabaseError from extra.dbwire import InterfaceError from extra.dbwire import NotSupportedError from extra.dbwire import OperationalError from extra.dbwire import ProgrammingError _MAX_MESSAGE_LENGTH = 0x40000000 # packet types _PKT_SQL_BATCH = 0x01 _PKT_LOGIN7 = 0x10 _PKT_PRELOGIN = 0x12 _STATUS_EOM = 0x01 def _u8(data, off): return struct.unpack("= len(data) header = struct.pack(">BBHHBB", mtype, _STATUS_EOM if last else 0x00, len(chunk) + 8, 0, packet_id & 0xff, 0) sock.sendall(header + chunk) packet_id += 1 if last: break def _read_message(sock): # reassemble a full TDS message across packets (EOM status bit marks the last) body = b"" while True: header = _recvn(sock, 8) mtype, status, length = struct.unpack(">BBH", header[:4]) if length < 8 or length > _MAX_MESSAGE_LENGTH: raise InterfaceError("invalid TDS packet length (%d)" % length) body += _recvn(sock, length - 8) if status & _STATUS_EOM: break return body # ---- PRELOGIN ---------------------------------------------------------------------------------------- def _prelogin(sock): ver = struct.pack(">IH", 0x11000000, 0) enc = b"\x02" # ENCRYPT_NOT_SUP tokens = b"\x00" + struct.pack(">HH", 11, len(ver)) tokens += b"\x01" + struct.pack(">HH", 11 + len(ver), len(enc)) tokens += b"\xff" _send_message(sock, _PKT_PRELOGIN, tokens + ver + enc) body = _read_message(sock) off = 0 while off < len(body) and _u8(body, off) != 0xff: token = _u8(body, off) toff, tlen = struct.unpack(">HH", body[off + 1:off + 5]) if token == 0x01 and _u8(body, toff) == 0x03: # server requires encryption raise NotSupportedError("server requires TDS encryption; the dependency-free client supports cleartext only") off += 5 # ---- LOGIN7 ------------------------------------------------------------------------------------------ def _encode_password(password): out = bytearray() for b in bytearray(password.encode("utf-16-le")): b = ((b << 4) & 0xf0) | ((b >> 4) & 0x0f) out.append(b ^ 0xa5) return bytes(out) def _login7(sock, user, password, database, hostname="dbwire", appname="dbwire"): fields = [ hostname.encode("utf-16-le"), (user or "").encode("utf-16-le"), _encode_password(password or ""), appname.encode("utf-16-le"), b"", # server name b"", # (extension / unused) "dbwire".encode("utf-16-le"), # client interface name b"", # language (database or "").encode("utf-16-le"), ] char_counts = [6, len(user or ""), len(password or ""), 6, 0, 0, 6, 0, len(database or "")] base = 94 # fixed header (36) + offset/length block (58) var, offsets, cursor = b"", b"", base for i, data in enumerate(fields): offsets += struct.pack("= 0 else ("-", -offset) s += " %s%02d:%02d" % (sign, mins // 60, mins % 60) return s # SQL Server COLLATION -> Python codec. The 5-byte collation is a little-endian uint32 (low 20 bits = LCID) # plus a 1-byte sort id: a non-zero sort id fixes the code page, else the LCID does. Only single-byte / DBCS # code pages need a codec (NVARCHAR is UTF-16, handled separately). Derived from pytds; default cp1252 (the # stock SQL_Latin1_General code page - NOT latin-1, whose 0x80-0x9F differ, corrupting e.g. the euro sign). _LCID_CP = { 0x405: "cp1250", 0x40e: "cp1250", 0x415: "cp1250", 0x418: "cp1250", 0x41a: "cp1250", 0x41b: "cp1250", 0x41c: "cp1250", 0x424: "cp1250", 0x402: "cp1251", 0x419: "cp1251", 0x422: "cp1251", 0x423: "cp1251", 0x42f: "cp1251", 0x408: "cp1253", 0x41f: "cp1254", 0x42c: "cp1254", 0x443: "cp1254", 0x40d: "cp1255", 0x401: "cp1256", 0x420: "cp1256", 0x429: "cp1256", 0x425: "cp1257", 0x426: "cp1257", 0x427: "cp1257", 0x42a: "cp1258", 0x41e: "cp874", 0x411: "cp932", 0x804: "cp936", 0x1004: "cp936", 0x412: "cp949", 0x404: "cp950", 0xc04: "cp950", 0x1404: "cp950", } def _sortid_cp(sid): if 30 <= sid <= 34: return "cp437" if 40 <= sid <= 44 or sid == 49 or 55 <= sid <= 61: return "cp850" if sid in (51, 52, 53, 54) or 183 <= sid <= 186: return "cp1252" if 80 <= sid <= 96: return "cp1250" if 104 <= sid <= 108: return "cp1251" if 112 <= sid <= 124: return "cp1253" if 128 <= sid <= 130: return "cp1254" if 136 <= sid <= 138: return "cp1255" if 144 <= sid <= 146: return "cp1256" if 152 <= sid <= 160: return "cp1257" return None def _collation_codec(collation): if not collation or len(collation) < 5: return "cp1252" lump = struct.unpack(" raw bytes if base in (0xa7, 0xaf): # (var)char: metadata = 5-byte collation + 2-byte max length return val.decode(_collation_codec(meta[:5]), "replace") if base == 0x28: return _decode_temporal(base, 0, val) if base in (0x29, 0x2a, 0x2b): # metadata = scale return _decode_temporal(base, bytearray(meta)[0], val) return "".join("%02x" % x for x in bytearray(val)) # unknown base type -> hex (never desyncs) class _Column(object): __slots__ = ("name", "type", "size", "scale", "binary", "collation") def _parse_type_info(data, off): col = _Column() col.type = _u8(data, off); off += 1 col.size, col.scale, col.binary, col.collation = 0, 0, False, None t = col.type if t in (0x30, 0x32, 0x34, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x7a, 0x7f, 0x1f): pass # fixed-length types, size implied by type elif t in (0x26, 0x68, 0x6d, 0x6e, 0x6f, 0x24): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID col.size = _u8(data, off); off += 1 elif t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN/NUMERICN + legacy DECIMAL/NUMERIC (size, precision, scale) col.size = _u8(data, off); off += 1 off += 1 # precision col.scale = _u8(data, off); off += 1 elif t in (0xa7, 0xaf, 0xe7, 0xef): # (BIG)VARCHAR/CHAR, N(VAR)CHAR col.size = struct.unpack(" raw bytes if t in (0xe7, 0xef): return raw.decode("utf-16-le", "replace"), off return raw.decode(_collation_codec(col.collation), "replace"), off # (var)char: the collation's code page if t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE: 1-byte textptr len (0 = NULL) then textptr+timestamp then 4-byte len ptr_len = _u8(data, off); off += 1 if ptr_len == 0: return None, off off += ptr_len + 8 (n,) = struct.unpack(" sqlmap hex-encodes them return _read_plp(data, off) if t == 0x62: # SQL_VARIANT: 4-byte total length (0 = NULL) then a self-describing value body (total,) = struct.unpack("= 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, sock): self._sock = sock def cursor(self): return Cursor(self) def commit(self): pass # sqlmap issues autonomous statements; SET IMPLICIT_TRANSACTIONS is off by default def rollback(self): pass def close(self): try: self._sock.close() except Exception: pass def _query(self, query): # TDS 7.2+ SQL batch must be prefixed with ALL_HEADERS carrying the transaction descriptor header headers = struct.pack("