mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
307 lines
14 KiB
Python
307 lines
14 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 PostgreSQL frontend/backend protocol v3 client (stdlib only).
|
|
|
|
Covers the whole PostgreSQL-wire family (PostgreSQL, CockroachDB, CrateDB, Redshift, Greenplum, Vertica).
|
|
Auth: trust / cleartext / MD5 / SCRAM-SHA-256 (modern default). Uses the *simple query* protocol, whose
|
|
per-message implicit transaction auto-commits - so it is immune to the aborted-transaction poisoning and
|
|
commit-before-fetch pitfalls that bite the stateful native drivers. Binary (bytea) values arrive as the
|
|
server's readable '\\xHEX' text (text result format), so no memoryview/blob corruption either.
|
|
"""
|
|
|
|
import base64
|
|
import binascii
|
|
import hashlib
|
|
import hmac
|
|
import os
|
|
import socket
|
|
import struct
|
|
|
|
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 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 = {
|
|
"22": DataError, "23": IntegrityError,
|
|
"08": OperationalError, "28": OperationalError, "53": OperationalError,
|
|
"57": OperationalError, "58": OperationalError,
|
|
}
|
|
|
|
def _xor(a, b):
|
|
# byte-wise XOR of two equal-length byte strings (Python 2 and 3 safe)
|
|
if str is bytes: # Python 2: iterating bytes yields 1-char strings
|
|
return b"".join(chr(ord(x) ^ ord(y)) for x, y in zip(a, b))
|
|
return bytes(x ^ y for x, y in zip(a, b))
|
|
|
|
def _recvn(sock, n):
|
|
buf = b""
|
|
while len(buf) < n:
|
|
chunk = sock.recv(n - len(buf))
|
|
if not chunk:
|
|
raise InterfaceError("connection closed by server")
|
|
buf += chunk
|
|
return buf
|
|
|
|
def _read_message(sock):
|
|
mtype = _recvn(sock, 1)
|
|
(length,) = struct.unpack("!I", _recvn(sock, 4))
|
|
if length < 4 or length > _MAX_MESSAGE_LENGTH:
|
|
raise InterfaceError("invalid backend message length (%d)" % length)
|
|
return mtype, _recvn(sock, length - 4)
|
|
|
|
def _send(sock, mtype, payload):
|
|
sock.sendall((mtype or b"") + struct.pack("!I", len(payload) + 4) + payload)
|
|
|
|
def _error_message(payload):
|
|
# ErrorResponse/NoticeResponse: series of (byte field-code, cstring value), terminated by a NUL byte.
|
|
# Returns (human message, SQLSTATE). Tolerant of a truncated/unterminated stream (find() not index()).
|
|
fields, off = {}, 0
|
|
while off < len(payload) and payload[off:off + 1] != b"\x00":
|
|
code = payload[off:off + 1]
|
|
end = payload.find(b"\x00", off + 1)
|
|
if end == -1:
|
|
break
|
|
fields[code] = payload[off + 1:end].decode("utf-8", "replace")
|
|
off = end + 1
|
|
return fields.get(b"M", "unknown error"), fields.get(b"C", "")
|
|
|
|
def _raise_server_error(message, sqlstate):
|
|
raise _SQLSTATE_CLASS.get((sqlstate or "")[:2], ProgrammingError)("(remote) %s" % 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 # reset before (a failed) query
|
|
self.description, self._rows, self._pos, self.rowcount = self.connection._simple_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, 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)
|
|
|
|
def commit(self):
|
|
pass # simple-query protocol commits each statement implicitly
|
|
|
|
def rollback(self):
|
|
pass
|
|
|
|
def close(self):
|
|
try:
|
|
_send(self._sock, b"X", b"") # Terminate
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self._sock.close()
|
|
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
|
|
while True:
|
|
mtype, payload = _read_message(self._sock)
|
|
try:
|
|
if mtype == b"T": # RowDescription (a new result set: reset rows so we return only the last one)
|
|
(count,) = struct.unpack("!H", payload[:2])
|
|
description, rows, rowcount, off = [], [], -1, 2
|
|
for _ in range(count):
|
|
end = payload.index(b"\x00", off)
|
|
name = payload[off:end].decode("utf-8", "replace")
|
|
off = end + 1
|
|
(typeoid,) = struct.unpack("!I", payload[off + 6:off + 10])
|
|
off += 18 # tableoid4 colno2 typeoid4 typelen2 typmod4 format2
|
|
description.append((name, typeoid, None, None, None, None, None))
|
|
elif mtype == b"D": # DataRow
|
|
(count,) = struct.unpack("!H", payload[:2])
|
|
off, row = 2, []
|
|
for col in range(count):
|
|
(vlen,) = struct.unpack("!i", payload[off:off + 4])
|
|
off += 4
|
|
if vlen == -1:
|
|
row.append(None)
|
|
else:
|
|
if off + vlen > len(payload):
|
|
raise InterfaceError("truncated DataRow")
|
|
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()
|
|
if tag and tag[-1].isdigit():
|
|
rowcount = int(tag[-1])
|
|
elif mtype == b"G": # CopyInResponse - server now waits for client CopyData; refuse to avoid a deadlock
|
|
_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); 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:
|
|
raise InterfaceError("malformed backend message: %s" % ex)
|
|
if error is not None:
|
|
_raise_server_error(*error)
|
|
return description, rows, 0, rowcount
|
|
|
|
def _authenticate(sock, user, password):
|
|
cfirst_bare = None
|
|
while True:
|
|
mtype, payload = _read_message(sock)
|
|
if mtype in (b"N", b"S"): # NoticeResponse / ParameterStatus may legally precede AuthenticationOk
|
|
continue
|
|
if mtype == b"E":
|
|
_raise_server_error_as_operational(payload)
|
|
if mtype != b"R":
|
|
raise InterfaceError("unexpected message %r during authentication" % mtype)
|
|
(code,) = struct.unpack("!I", payload[:4])
|
|
if code == 0: # AuthenticationOk (also the trust case)
|
|
return
|
|
elif code == 3: # cleartext password
|
|
_send(sock, b"p", (password or "").encode("utf-8") + b"\x00")
|
|
elif code == 5: # MD5 password
|
|
salt = payload[4:8]
|
|
inner = hashlib.md5((password or "").encode("utf-8") + (user or "").encode("utf-8")).hexdigest()
|
|
token = b"md5" + hashlib.md5(inner.encode("ascii") + salt).hexdigest().encode("ascii")
|
|
_send(sock, b"p", token + b"\x00")
|
|
elif code == 10: # SASL (SCRAM-SHA-256)
|
|
if not hasattr(hashlib, "pbkdf2_hmac"):
|
|
raise NotSupportedError("SCRAM-SHA-256 authentication requires Python >= 2.7.8 (hashlib.pbkdf2_hmac)")
|
|
nonce = base64.b64encode(os.urandom(18)).decode("ascii")
|
|
cfirst_bare = "n=,r=%s" % nonce
|
|
client_first = "n,," + cfirst_bare
|
|
_send(sock, b"p", b"SCRAM-SHA-256\x00" + struct.pack("!I", len(client_first)) + client_first.encode("ascii"))
|
|
elif code == 11: # SASLContinue (server-first)
|
|
try:
|
|
server_first = payload[4:].decode("ascii")
|
|
attrs = dict(kv.split("=", 1) for kv in server_first.split(","))
|
|
snonce, salt, iterations = attrs["r"], base64.b64decode(attrs["s"]), int(attrs["i"])
|
|
except (KeyError, ValueError, binascii.Error, UnicodeDecodeError) as ex:
|
|
raise OperationalError("malformed SCRAM server-first message (%s)" % ex)
|
|
salted = hashlib.pbkdf2_hmac("sha256", (password or "").encode("utf-8"), salt, iterations)
|
|
client_key = hmac.new(salted, b"Client Key", hashlib.sha256).digest()
|
|
stored_key = hashlib.sha256(client_key).digest()
|
|
client_final_noproof = "c=biws,r=%s" % snonce
|
|
auth_message = "%s,%s,%s" % (cfirst_bare, server_first, client_final_noproof)
|
|
client_sig = hmac.new(stored_key, auth_message.encode("ascii"), hashlib.sha256).digest()
|
|
proof = base64.b64encode(_xor(client_key, client_sig)).decode("ascii")
|
|
_send(sock, b"p", ("%s,p=%s" % (client_final_noproof, proof)).encode("ascii"))
|
|
elif code == 12: # SASLFinal
|
|
pass
|
|
else:
|
|
raise InterfaceError("unsupported authentication request %d" % code)
|
|
|
|
def _raise_server_error_as_operational(payload):
|
|
message, _ = _error_message(payload)
|
|
raise OperationalError("(remote) %s" % message)
|
|
|
|
def connect(host=None, port=5432, user=None, password=None, database=None, connect_timeout=None, **kwargs):
|
|
try:
|
|
sock = socket.create_connection((host or "localhost", int(port or 5432)), timeout=connect_timeout)
|
|
sock.settimeout(None)
|
|
except (socket.error, socket.timeout) as ex:
|
|
raise OperationalError("could not connect to '%s:%s' (%s)" % (host, port, ex))
|
|
|
|
params = b""
|
|
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:
|
|
_authenticate(sock, user, password)
|
|
while True: # drain until ReadyForQuery (ParameterStatus/BackendKeyData/NoticeResponse)
|
|
mtype, payload = _read_message(sock)
|
|
if mtype == b"E":
|
|
_raise_server_error_as_operational(payload)
|
|
if mtype == b"Z":
|
|
break
|
|
except Exception: # any setup failure (DB-API or otherwise) must still close the socket
|
|
try:
|
|
sock.close()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
|
|
return Connection(sock)
|