#!/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'; 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)