mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-12 02:22:55 +00:00
430 lines
19 KiB
Python
430 lines
19 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 Sybase ASE client speaking TDS 5.0 (stdlib only, no pymssql/FreeTDS).
|
|
|
|
Sybase and Microsoft share the 8-byte TDS packet framing (reused from tds.py) and part company at the
|
|
login: ASE speaks TDS 5.0, which means a fixed-layout LOGINREC instead of LOGIN7, a capability
|
|
negotiation, SQL sent as a LANGUAGE token in a normal packet, and results described by ROWFMT rather than
|
|
COLMETADATA. Cleartext login only. Read-oriented for sqlmap: execute() takes a fully-formed query string,
|
|
binary values come back as bytes (sqlmap hex-encodes them).
|
|
|
|
Two encodings look like their Microsoft namesakes and are not: DECIMAL/NUMERIC carries a big-endian
|
|
magnitude with 1 meaning *negative* (Microsoft is little-endian with 1 meaning positive), and DONE reports
|
|
a 4-byte row count (Microsoft uses 8).
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import socket
|
|
import struct
|
|
|
|
from extra.dbwire import DatabaseError
|
|
from extra.dbwire import DataError
|
|
from extra.dbwire import IntegrityError
|
|
from extra.dbwire import InterfaceError
|
|
from extra.dbwire import NotSupportedError
|
|
from extra.dbwire import OperationalError
|
|
from extra.dbwire import handshake_done
|
|
from extra.dbwire import keepalive
|
|
from extra.dbwire import ProgrammingError
|
|
|
|
# the packet framing is byte-identical to the Microsoft dialect - only what travels inside it differs
|
|
from extra.dbwire.tds import _decode_datetime
|
|
from extra.dbwire.tds import _decode_money
|
|
from extra.dbwire.tds import _decode_smalldatetime
|
|
from extra.dbwire.tds import _read_message
|
|
from extra.dbwire.tds import _send_message
|
|
|
|
_PKT_LOGIN = 0x02
|
|
_PKT_NORMAL = 0x0f
|
|
_LOGIN_CHUNK = 504 # the login travels in 512-byte packets: the server's buffer is not negotiated yet
|
|
_PACKET_SIZE = 2048 # ASE's default 'max network packet size'; a bigger request is negotiated down
|
|
_HEADER_SIZE = 8
|
|
|
|
# tokens
|
|
_TOKEN_LANGUAGE = 0x21
|
|
_TOKEN_ROWFMT = 0xee
|
|
_TOKEN_ROWFMT2 = 0x61
|
|
_TOKEN_ROW = 0xd1
|
|
_TOKEN_CAPABILITY = 0xe2
|
|
_TOKEN_ENVCHANGE = 0xe3
|
|
_TOKEN_EED = 0xe5
|
|
_TOKEN_RETURNSTATUS = 0x79
|
|
_TOKEN_LOGOUT = 0x71
|
|
_TOKEN_DONE = frozenset((0xfd, 0xfe, 0xff)) # DONE / DONEPROC / DONEINPROC
|
|
_DONE_COUNT = 0x0010 # DONE status bit: the row count is meaningful
|
|
_ENV_PACKET_SIZE = 4 # ENVCHANGE type: the server's answer to the requested size
|
|
|
|
# SYB* datatype codes. Fixed-length types carry no per-value length; everything else is length-prefixed,
|
|
# and a zero length means NULL (TDS 5 cannot tell an empty string from NULL - neither can ASE itself).
|
|
_T_INT1, _T_BIT, _T_INT2, _T_INT4, _T_INT8 = 0x30, 0x32, 0x34, 0x38, 0xbf
|
|
_T_REAL, _T_FLT8, _T_MONEY, _T_MONEY4 = 0x3b, 0x3e, 0x3c, 0x7a
|
|
_T_DATETIME, _T_DATETIME4, _T_DATE, _T_TIME = 0x3d, 0x3a, 0x31, 0x33
|
|
_T_INTN, _T_FLTN, _T_MONEYN, _T_DATETIMN, _T_DATEN, _T_TIMEN = 0x26, 0x6d, 0x6e, 0x6f, 0x7b, 0x93
|
|
_T_DECIMAL, _T_NUMERIC = 0x6a, 0x6c
|
|
_T_CHAR, _T_VARCHAR, _T_BINARY, _T_VARBINARY, _T_NVARCHAR = 0x2f, 0x27, 0x2d, 0x25, 0x67
|
|
_T_TEXT, _T_IMAGE, _T_UNITEXT = 0x23, 0x22, 0xae
|
|
_T_LONGBINARY, _T_LONGCHAR = 0xe1, 0xaf
|
|
_T_BIGDATETIME, _T_BIGTIME = 0xbb, 0xbc
|
|
_T_BIGDATETIMEN, _T_BIGTIMEN = 0xbd, 0xbe
|
|
_T_VOID = 0x1f
|
|
|
|
_FIXED_LENGTH = {
|
|
_T_INT1: 1, _T_BIT: 1, _T_INT2: 2, _T_INT4: 4, _T_INT8: 8, _T_REAL: 4, _T_FLT8: 8,
|
|
_T_MONEY: 8, _T_MONEY4: 4, _T_DATETIME: 8, _T_DATETIME4: 4, _T_DATE: 4, _T_TIME: 4,
|
|
_T_BIGDATETIME: 8, _T_BIGTIME: 8, _T_VOID: 0,
|
|
}
|
|
_BLOB_TYPES = frozenset((_T_TEXT, _T_IMAGE, _T_UNITEXT)) # textptr + timestamp + 4-byte length
|
|
_LONG_TYPES = frozenset((_T_LONGBINARY, _T_LONGCHAR)) # 4-byte length
|
|
_BINARY_TYPES = frozenset((_T_BINARY, _T_VARBINARY, _T_IMAGE))
|
|
_DECIMAL_TYPES = frozenset((_T_DECIMAL, _T_NUMERIC))
|
|
# univarchar/unichar/unitext are carried as LONGBINARY/UNITEXT holding UTF-16 - only the user type says so
|
|
_UNICODE_USERTYPES = frozenset((34, 35, 36))
|
|
# a client that does not claim the date/time/bigdatetime capabilities gets those columns converted to a
|
|
# plain datetime, which is lossless but would render a date as '... 00:00:00'. The user type survives the
|
|
# conversion, so it is what says how much of the value the column actually holds.
|
|
_USERTYPE_DATE = 37
|
|
_USERTYPE_TIME = frozenset((38, 49))
|
|
|
|
_IDENTIFIER = re.compile(r"^[A-Za-z_#][A-Za-z0-9_#$]*$")
|
|
|
|
def _u8(data, off):
|
|
return struct.unpack("<B", data[off:off + 1])[0]
|
|
|
|
def _u16(data, off):
|
|
return struct.unpack("<H", data[off:off + 2])[0]
|
|
|
|
def _i32(data, off):
|
|
return struct.unpack("<i", data[off:off + 4])[0]
|
|
|
|
def _login_string(value, size):
|
|
# fixed-width field: `size` bytes of (truncated, NUL-padded) text followed by the actual length
|
|
data = (value or "").encode("utf-8")[:size]
|
|
return data + b"\x00" * (size - len(data)) + struct.pack("<B", len(data))
|
|
|
|
def _loginrec(host, user, password, appname, servername, charset, packetsize):
|
|
"""
|
|
The TDS 5.0 LOGINREC: one fixed 568-byte record, then a 35-byte CAPABILITY token.
|
|
|
|
Field order and widths mirror what ASE's own Open Client puts on the wire. The two reserved runs are
|
|
spare in every implementation that documents them (Open Client sets one byte inside the second run,
|
|
ASE accepts the record without it), so they go out as zeros rather than as an unexplained constant.
|
|
"""
|
|
|
|
out = _login_string(host, 30) # client host name
|
|
out += _login_string(user, 30) # user name
|
|
out += _login_string(password, 30) # password (cleartext)
|
|
out += _login_string("%d" % os.getpid(), 30) # client process id, as text
|
|
# number/char/date byte orders, then: notify on USE, refuse dump-load & bulk-copy, interface, network
|
|
out += struct.pack("<9B", 3, 1, 6, 10, 9, 1, 1, 0, 0)
|
|
out += b"\x00" * 7 # spare
|
|
out += _login_string(appname, 30) # application name
|
|
out += _login_string(servername, 30) # server name
|
|
remote = b"\x00" + struct.pack("<B", len(password or "")) + (password or "").encode("utf-8")
|
|
out += remote + b"\x00" * (255 - len(remote)) + struct.pack("<B", len(remote)) # remote passwords
|
|
out += struct.pack("<4B", 5, 0, 0, 0) # TDS version 5.0.0.0
|
|
out += _login_string("dbwire", 10) # library name
|
|
out += struct.pack("<4B", 1, 0, 0, 0) # library version
|
|
out += struct.pack("<3B", 0, 0x0d, 0x11) # auto-convert short, float4 & date4 formats
|
|
out += b"\x00" # reserved
|
|
out += _login_string("", 30) # language (empty -> the server default)
|
|
out += b"\x00" * 13 # notify-on-language-change, security & HA fields
|
|
out += _login_string(charset, 30) # client character set
|
|
out += b"\x00" # notify on character-set change
|
|
out += _login_string("%d" % packetsize, 6) # network packet size, as text
|
|
out += b"\x00" * 4 # spare
|
|
|
|
# capability negotiation: request bits say what the client can do, response bits what it wants back.
|
|
# All-zero is the baseline dialect - the wide/streaming formats a fallback client has no use for are
|
|
# exactly the ones it must not claim, or the server starts answering in them.
|
|
capability = struct.pack("<3B", _TOKEN_CAPABILITY, 32, 0)
|
|
capability += struct.pack("<2B", 1, 14) + b"\x00" * 14
|
|
capability += struct.pack("<2B", 2, 14) + b"\x00" * 14
|
|
return out + capability
|
|
|
|
class _Column(object):
|
|
__slots__ = ("name", "type", "size", "scale", "usertype")
|
|
|
|
def _parse_rowfmt(data, off, length, codec):
|
|
end = off + length
|
|
count = _u16(data, off); off += 2
|
|
columns = []
|
|
for _ in range(count):
|
|
col = _Column()
|
|
namelen = _u8(data, off); off += 1
|
|
col.name = data[off:off + namelen].decode(codec, "replace"); off += namelen
|
|
off += 1 # status (nullability - the value length says it)
|
|
col.usertype = _i32(data, off); off += 4
|
|
col.type = _u8(data, off); off += 1
|
|
col.size, col.scale = _FIXED_LENGTH.get(col.type, 0), 0
|
|
if col.type in _DECIMAL_TYPES:
|
|
col.size = _u8(data, off)
|
|
col.scale = _u8(data, off + 2) # precision (off + 1) is not needed to decode
|
|
off += 3
|
|
elif col.type in _BLOB_TYPES:
|
|
col.size = _i32(data, off); off += 4
|
|
off += 2 + _u16(data, off) # the blob's table name
|
|
elif col.type in _LONG_TYPES:
|
|
col.size = _i32(data, off); off += 4
|
|
elif col.type not in _FIXED_LENGTH:
|
|
col.size = _u8(data, off); off += 1
|
|
off += 1 + _u8(data, off) # locale
|
|
columns.append(col)
|
|
if off != end:
|
|
raise InterfaceError("malformed ROWFMT token (%d columns did not fill %d bytes)" % (count, length))
|
|
return columns, off
|
|
|
|
def _decode_numeric(raw, scale):
|
|
# sign byte then a BIG-endian magnitude, and 1 means NEGATIVE (the Microsoft encoding is the mirror
|
|
# image of this: little-endian, 1 == positive)
|
|
magnitude = 0
|
|
for b in bytearray(raw[1:]):
|
|
magnitude = (magnitude << 8) | b
|
|
value = -magnitude if bytearray(raw)[0] else magnitude
|
|
if scale:
|
|
text = "%0*d" % (scale + 1, abs(value))
|
|
return ("-" if value < 0 else "") + text[:-scale] + "." + text[-scale:]
|
|
return "%d" % value
|
|
|
|
def _decode_date(raw):
|
|
import datetime
|
|
return "%s" % (datetime.date(1900, 1, 1) + datetime.timedelta(days=_i32(raw, 0)))
|
|
|
|
def _decode_time(raw):
|
|
import datetime
|
|
ticks = struct.unpack("<I", raw)[0] # 300ths of a second since midnight
|
|
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(milliseconds=ticks * 10.0 / 3.0)).time()
|
|
|
|
def _decode_bigdatetime(raw, time_only=False):
|
|
import datetime
|
|
micros = struct.unpack("<Q", raw)[0] # microseconds since 0001-01-01 00:00:00
|
|
value = datetime.datetime(1, 1, 1) + datetime.timedelta(microseconds=micros)
|
|
return "%s" % (value.time() if time_only else value)
|
|
|
|
def _decode_value(col, raw, codec):
|
|
t = col.type
|
|
if t in (_T_INT1, _T_BIT) or (t == _T_INTN and len(raw) == 1):
|
|
value = bytearray(raw)[0] # tinyint is unsigned; bit is 0/1
|
|
return "%d" % value
|
|
if t == _T_INT2 or (t == _T_INTN and len(raw) == 2):
|
|
return "%d" % struct.unpack("<h", raw)[0]
|
|
if t == _T_INT4 or (t == _T_INTN and len(raw) == 4):
|
|
return "%d" % struct.unpack("<i", raw)[0]
|
|
if t == _T_INT8 or (t == _T_INTN and len(raw) == 8):
|
|
return "%d" % struct.unpack("<q", raw)[0]
|
|
if t in (_T_REAL, _T_FLT8, _T_FLTN):
|
|
return repr(struct.unpack("<f" if len(raw) == 4 else "<d", raw)[0])
|
|
if t in (_T_MONEY, _T_MONEY4, _T_MONEYN):
|
|
return _decode_money(raw)
|
|
if t in _DECIMAL_TYPES:
|
|
return _decode_numeric(raw, col.scale)
|
|
if t in (_T_DATETIME, _T_DATETIME4, _T_DATETIMN):
|
|
text = _decode_datetime(raw) if len(raw) == 8 else _decode_smalldatetime(raw)
|
|
if col.usertype == _USERTYPE_DATE:
|
|
return text.split(" ")[0]
|
|
if col.usertype in _USERTYPE_TIME:
|
|
return text.split(" ")[1]
|
|
return text
|
|
if t in (_T_DATE, _T_DATEN):
|
|
return _decode_date(raw)
|
|
if t in (_T_TIME, _T_TIMEN):
|
|
return _decode_time(raw)
|
|
if t in (_T_BIGDATETIME, _T_BIGDATETIMEN):
|
|
return _decode_bigdatetime(raw)
|
|
if t in (_T_BIGTIME, _T_BIGTIMEN):
|
|
return _decode_bigdatetime(raw, time_only=True)
|
|
if t in _BINARY_TYPES:
|
|
return raw # sqlmap hex-encodes binary columns
|
|
if t == _T_UNITEXT or col.usertype in _UNICODE_USERTYPES:
|
|
return raw.decode("utf-16-le", "replace")
|
|
if t in (_T_CHAR, _T_VARCHAR, _T_NVARCHAR, _T_LONGCHAR, _T_TEXT):
|
|
return raw.decode(codec, "replace")
|
|
return raw # LONGBINARY that is not unichar, a locator handle, an unmapped type: bytes, not a guess
|
|
|
|
def _read_value(col, data, off, codec):
|
|
if col.type in _FIXED_LENGTH:
|
|
size = _FIXED_LENGTH[col.type]
|
|
raw, off = data[off:off + size], off + size
|
|
if not size:
|
|
return None, off
|
|
elif col.type in _BLOB_TYPES:
|
|
ptrlen = _u8(data, off); off += 1
|
|
if not ptrlen:
|
|
return None, off # no text pointer -> NULL
|
|
off += ptrlen + 8 # text pointer + timestamp
|
|
length = _i32(data, off); off += 4
|
|
raw, off = data[off:off + length], off + length
|
|
elif col.type in _LONG_TYPES:
|
|
length = _i32(data, off); off += 4
|
|
if not length:
|
|
return None, off
|
|
raw, off = data[off:off + length], off + length
|
|
else:
|
|
length = _u8(data, off); off += 1
|
|
if not length:
|
|
return None, off
|
|
raw, off = data[off:off + length], off + length
|
|
return _decode_value(col, raw, codec), off
|
|
|
|
def _eed(data, off, codec):
|
|
# EED: number(4) state(1) class(1) sqlstate, status(1) transtate(2) message, server, procedure, line
|
|
number = _i32(data, off)
|
|
severity = _u8(data, off + 5)
|
|
pos = off + 6
|
|
pos += 1 + _u8(data, pos) # SQLSTATE
|
|
pos += 1 + 2 # status + transaction state
|
|
length = _u16(data, pos); pos += 2
|
|
return number, severity, data[pos:pos + length].decode(codec, "replace").strip()
|
|
|
|
def _raise_server_error(number, message):
|
|
if number in (2601, 2615, 2627, 1105, 546, 547): # duplicate key/row, constraint, foreign key
|
|
raise IntegrityError(message)
|
|
if number in (247, 249, 257, 260, 264, 512, 8115): # conversion, overflow, arithmetic
|
|
raise DataError(message)
|
|
raise ProgrammingError(message)
|
|
|
|
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, affected = self.connection._query(query)
|
|
self.rowcount = len(self._rows) if self.description is not None else (affected if affected is not None else -1)
|
|
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, sock, codec):
|
|
self._sock = sock
|
|
self._codec = codec
|
|
# a packet larger than the size agreed at login is not an error the server reports - it drops the
|
|
# connection, so every send has to be chopped to what the login negotiated (ENVCHANGE 4 below)
|
|
self._chunk = _PACKET_SIZE - _HEADER_SIZE
|
|
|
|
def cursor(self):
|
|
return Cursor(self)
|
|
|
|
def commit(self):
|
|
pass # sqlmap issues autonomous statements; ASE is in unchained (auto-commit) mode by default
|
|
|
|
def rollback(self):
|
|
pass
|
|
|
|
def close(self):
|
|
try:
|
|
_send_message(self._sock, _PKT_NORMAL, struct.pack("<2B", _TOKEN_LOGOUT, 0))
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._sock.close()
|
|
except Exception:
|
|
pass
|
|
|
|
def _query(self, query):
|
|
# LANGUAGE token: length covers the status byte and the (single-byte encoded) statement text
|
|
text = query.encode(self._codec, "replace")
|
|
token = struct.pack("<BiB", _TOKEN_LANGUAGE, len(text) + 1, 0) + text
|
|
_send_message(self._sock, _PKT_NORMAL, token, self._chunk)
|
|
try:
|
|
return self._read_response()
|
|
except (struct.error, IndexError, ValueError, KeyError) as ex:
|
|
raise InterfaceError("malformed server response: %s" % ex)
|
|
|
|
def _read_response(self, login=False):
|
|
data = _read_message(self._sock)
|
|
off, columns, rows, description, affected = 0, [], [], None, None
|
|
error = None
|
|
while off < len(data):
|
|
token = _u8(data, off); off += 1
|
|
if token == _TOKEN_ROWFMT:
|
|
length = _u16(data, off); off += 2
|
|
columns, off = _parse_rowfmt(data, off, length, self._codec)
|
|
description = [(c.name, c.type, None, None, None, c.scale, None) for c in columns]
|
|
rows, affected = [], None
|
|
elif token == _TOKEN_ROW:
|
|
row = []
|
|
for col in columns:
|
|
value, off = _read_value(col, data, off, self._codec)
|
|
row.append(value)
|
|
rows.append(tuple(row))
|
|
elif token in _TOKEN_DONE:
|
|
status, _transtate, count = struct.unpack("<HHi", data[off:off + 8]); off += 8
|
|
if status & _DONE_COUNT:
|
|
affected = count if affected is None else affected + count
|
|
elif token == _TOKEN_EED:
|
|
length = _u16(data, off)
|
|
number, severity, message = _eed(data, off + 2, self._codec)
|
|
if severity > 10: # 10 and below are informational (e.g. 'Changed database')
|
|
error = (number, "(remote) %s" % message)
|
|
off += 2 + length
|
|
elif token == _TOKEN_ENVCHANGE:
|
|
length = _u16(data, off)
|
|
if _u8(data, off + 2) == _ENV_PACKET_SIZE:
|
|
size = data[off + 4:off + 4 + _u8(data, off + 3)]
|
|
self._chunk = max(_LOGIN_CHUNK, int(size) - _HEADER_SIZE)
|
|
off += 2 + length
|
|
elif token == _TOKEN_RETURNSTATUS:
|
|
off += 4
|
|
elif token == _TOKEN_ROWFMT2:
|
|
raise NotSupportedError("the server answered with the wide ROWFMT2 format")
|
|
else:
|
|
off += 2 + _u16(data, off) # every other TDS 5 token is 2-byte length prefixed
|
|
if error is not None:
|
|
if login:
|
|
raise OperationalError(error[1])
|
|
_raise_server_error(*error)
|
|
return description, rows, affected
|
|
|
|
def connect(host=None, port=5000, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
|
charset, codec = kwargs.get("charset", "utf8"), "utf-8"
|
|
try:
|
|
sock = socket.create_connection((host or "localhost", int(port or 5000)), timeout=connect_timeout)
|
|
keepalive(sock)
|
|
except (socket.error, socket.timeout) as ex:
|
|
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
|
|
|
connection = Connection(sock, codec)
|
|
try:
|
|
login = _loginrec("dbwire", user or "", password or "", "dbwire", host or "localhost", charset, _PACKET_SIZE)
|
|
_send_message(sock, _PKT_LOGIN, login, _LOGIN_CHUNK)
|
|
connection._read_response(login=True)
|
|
handshake_done(sock)
|
|
if database:
|
|
if not _IDENTIFIER.match(database):
|
|
raise ProgrammingError("unsupported database name %r" % database)
|
|
connection._query("USE %s" % database)
|
|
except (DatabaseError, InterfaceError):
|
|
connection.close()
|
|
raise
|
|
except Exception as ex:
|
|
connection.close()
|
|
raise OperationalError("Sybase login failed (%s)" % ex)
|
|
return connection
|