mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-08-04 14:55:40 +00:00
297 lines
11 KiB
Python
297 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
|
See the file 'LICENSE' for copying permission
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
import re
|
|
import struct
|
|
|
|
from lib.core.data import conf
|
|
from lib.core.data import kb
|
|
from lib.core.enums import HTTP_HEADER
|
|
from thirdparty.six.moves.urllib.parse import unquote
|
|
|
|
# gRPC-Web body support (grpc-web-text / base64, unary only). A gRPC-Web message is a length-prefixed
|
|
# protobuf frame; grpc-web-text is that frame base64-encoded. Because protobuf is length-prefixed and
|
|
# sqlmap injects by appending, the body is decoded to a JSON view of its (heuristically-detected) string
|
|
# fields so the existing JSON injection engine handles marking/placement, then re-encoded with corrected
|
|
# length prefixes at send time (see connect.py). NOT supported (deliberately not detected, never
|
|
# corrupted): binary application/grpc-web+proto, compression (grpc-encoding), and streaming.
|
|
|
|
CONTENT_TYPE = "application/grpc-web-text"
|
|
CONTENT_TYPE_PROTO = "application/grpc-web-text+proto" # equivalent spelling (message format hint)
|
|
_MAX_VARINT_BYTES = 10 # a 64-bit varint is at most 10 bytes
|
|
_MAX_FIELD_NUMBER = 0x1fffffff # protobuf maximum field number (2**29 - 1)
|
|
_BASE64_QUANTUM_REGEX = re.compile(r"\A(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)\Z")
|
|
|
|
def _b64decode(text):
|
|
# strict, py2/py3-safe (no 'validate=' kwarg): reject whitespace/invalid chars, but decode per 4-char
|
|
# quantum so INDEPENDENTLY-padded base64 chunks (allowed even for a unary text response) reconstruct
|
|
# correctly - a mid-stream padded quantum is fine, arbitrary mid-stream padding chars are not
|
|
if isinstance(text, bytes):
|
|
text = text.decode("ascii")
|
|
if len(text) % 4 != 0:
|
|
raise ValueError("invalid base64 length")
|
|
out = bytearray()
|
|
for offset in range(0, len(text), 4):
|
|
quantum = text[offset:offset + 4]
|
|
if not _BASE64_QUANTUM_REGEX.match(quantum):
|
|
raise ValueError("invalid base64")
|
|
out += base64.b64decode(quantum)
|
|
return bytes(out)
|
|
|
|
def _readVarint(buf, pos):
|
|
shift = result = 0
|
|
start = pos
|
|
while True:
|
|
if pos >= len(buf):
|
|
raise ValueError("truncated varint")
|
|
b = buf[pos] if isinstance(buf[pos], int) else ord(buf[pos])
|
|
pos += 1
|
|
result |= (b & 0x7f) << shift
|
|
if not (b & 0x80):
|
|
if result >= (1 << 64):
|
|
raise ValueError("varint exceeds 64 bits")
|
|
return result, pos
|
|
shift += 7
|
|
if pos - start >= _MAX_VARINT_BYTES:
|
|
raise ValueError("overlong varint")
|
|
|
|
def _writeVarint(n):
|
|
out = bytearray()
|
|
while True:
|
|
b = n & 0x7f
|
|
n >>= 7
|
|
out.append(b | 0x80 if n else b)
|
|
if not n:
|
|
return bytes(out)
|
|
|
|
def _readExact(buf, pos, length):
|
|
end = pos + length
|
|
if length < 0 or end > len(buf):
|
|
raise ValueError("truncated protobuf field")
|
|
return buf[pos:end], end
|
|
|
|
def _decode(buf):
|
|
buf = bytes(buf)
|
|
pos = 0
|
|
out = []
|
|
while pos < len(buf):
|
|
tag, pos = _readVarint(buf, pos)
|
|
fn, wt = tag >> 3, tag & 7
|
|
if fn == 0 or fn > _MAX_FIELD_NUMBER:
|
|
raise ValueError("invalid field number %d" % fn)
|
|
if wt == 0:
|
|
val, pos = _readVarint(buf, pos)
|
|
elif wt == 1:
|
|
val, pos = _readExact(buf, pos, 8)
|
|
elif wt == 2:
|
|
ln, pos = _readVarint(buf, pos)
|
|
val, pos = _readExact(buf, pos, ln)
|
|
elif wt == 5:
|
|
val, pos = _readExact(buf, pos, 4)
|
|
else:
|
|
raise ValueError("unsupported wire type %d" % wt)
|
|
out.append([fn, wt, val])
|
|
return out
|
|
|
|
def _encode(fields):
|
|
out = bytearray()
|
|
for fn, wt, val in fields:
|
|
out += _writeVarint((fn << 3) | wt)
|
|
if wt == 0:
|
|
out += _writeVarint(val)
|
|
elif wt == 2:
|
|
val = val if isinstance(val, bytes) else bytes(val)
|
|
out += _writeVarint(len(val)) + val
|
|
else:
|
|
out += val if isinstance(val, bytes) else bytes(val)
|
|
return bytes(out)
|
|
|
|
def _frame(msg):
|
|
return b"\x00" + struct.pack(">I", len(msg)) + msg
|
|
|
|
def _unframe(data):
|
|
# strict: exactly one uncompressed data frame (flag 0x00), nothing trailing (unary; no
|
|
# compression/streaming/reserved flag bits)
|
|
if len(data) < 5:
|
|
raise ValueError("truncated gRPC-Web frame")
|
|
flag = data[0] if isinstance(data[0], int) else ord(data[0])
|
|
if flag != 0x00:
|
|
raise ValueError("unsupported request frame flags 0x%02x" % flag)
|
|
length = struct.unpack(">I", data[1:5])[0]
|
|
if len(data) != 5 + length:
|
|
raise ValueError("incomplete/oversized gRPC-Web frame")
|
|
return data[5:5 + length]
|
|
|
|
def _isTextContentType(value):
|
|
return (value or "").split(";", 1)[0].strip().lower() in (CONTENT_TYPE, CONTENT_TYPE_PROTO)
|
|
|
|
def acceptsTextContentType(value):
|
|
# True if a (possibly comma-separated) Accept header already negotiates a grpc-web-text response
|
|
return any(_isTextContentType(_) for _ in (value or "").split(","))
|
|
|
|
def _stringFields(fields):
|
|
# Descriptorless: wire type 2 is string / bytes / embedded-message / packed - indistinguishable
|
|
# without the .proto. Offer every printable-UTF-8 length-delimited field as a candidate; do NOT try
|
|
# to exclude values that merely also parse as protobuf (ordinary strings like "A12345678"/"M1234" do,
|
|
# so excluding them silently drops real injection points). Non-selected fields stay in the skeleton
|
|
# untouched, so a mis-picked embedded message just fails to inject and is skipped - the safe failure.
|
|
retVal = []
|
|
for index, (fn, wt, val) in enumerate(fields):
|
|
if wt != 2:
|
|
continue
|
|
try:
|
|
value = bytes(val).decode("utf-8")
|
|
except Exception:
|
|
continue
|
|
if all(char in "\t\n" or ord(char) > 31 for char in value):
|
|
retVal.append((index, fn, value))
|
|
return retVal
|
|
|
|
def _requestContentType():
|
|
for header, value in (conf.httpHeaders or []):
|
|
if header.lower() == HTTP_HEADER.CONTENT_TYPE.lower():
|
|
return value
|
|
return ""
|
|
|
|
def _headerValue(headers, key):
|
|
if not headers:
|
|
return None
|
|
key = key.lower()
|
|
try:
|
|
items = headers.items()
|
|
except AttributeError:
|
|
items = headers
|
|
for header, value in items:
|
|
if header.lower() == key:
|
|
return value
|
|
return None
|
|
|
|
def decodeBody(data):
|
|
"""
|
|
Probe 'data' for a gRPC-Web (grpc-web-text) body WITHOUT any side effects. Returns a
|
|
(jsonView, skeleton) tuple - the JSON string of injectable string fields plus the message skeleton
|
|
to re-encode with - or (None, None). The caller commits the skeleton to kb.grpcWeb only on acceptance.
|
|
"""
|
|
|
|
if not _isTextContentType(_requestContentType()):
|
|
return None, None
|
|
|
|
try:
|
|
fields = _decode(_unframe(_b64decode(data)))
|
|
except Exception:
|
|
return None, None
|
|
|
|
strings = _stringFields(fields)
|
|
if not strings:
|
|
return None, None
|
|
|
|
counts = {}
|
|
for _, fn, _value in strings:
|
|
counts[fn] = counts.get(fn, 0) + 1
|
|
|
|
occurrence = {}
|
|
mapping = {}
|
|
view = {}
|
|
for index, fn, value in strings:
|
|
if counts[fn] == 1:
|
|
key = "f%d" % fn
|
|
else:
|
|
key = "f%d_%d" % (fn, occurrence.get(fn, 0))
|
|
occurrence[fn] = occurrence.get(fn, 0) + 1
|
|
mapping[key] = index
|
|
view[key] = value
|
|
|
|
skeleton = {"fields": [list(_) for _ in fields], "map": mapping}
|
|
|
|
return json.dumps(view), skeleton
|
|
|
|
def encodeBody(jsonBody):
|
|
"""
|
|
Inverse of decodeBody(): overlay the (possibly injected) JSON string values onto the skeleton and
|
|
re-encode a grpc-web-text (base64) body, recomputing the length prefixes. Called at send time.
|
|
|
|
Only a body whose keys are EXACTLY the gRPC surrogate keys is transformed - so unrelated JSON bodies
|
|
on the shared request path (CSRF/second-order/redirect/safe requests) pass through untouched.
|
|
"""
|
|
|
|
if not kb.grpcWeb:
|
|
return jsonBody
|
|
|
|
try:
|
|
parsed = json.loads(jsonBody)
|
|
except Exception:
|
|
return jsonBody
|
|
|
|
if not isinstance(parsed, dict) or set(parsed) != set(kb.grpcWeb["map"]):
|
|
return jsonBody
|
|
|
|
fields = [list(_) for _ in kb.grpcWeb["fields"]]
|
|
|
|
for key, index in kb.grpcWeb["map"].items():
|
|
value = parsed[key]
|
|
fields[index][2] = (value if hasattr(value, "encode") else str(value)).encode("utf-8")
|
|
|
|
return base64.b64encode(_frame(_encode(fields))).decode("ascii")
|
|
|
|
def decodeResponse(page, responseHeaders=None):
|
|
"""
|
|
Render a gRPC-Web response as readable text (message-frame fields + the trailer frame's
|
|
grpc-status/grpc-message = the back-end error) so the oracle/error-regex/in-band paths see content,
|
|
not an opaque blob. Handles trailers-only errors (status/message in response headers, empty body).
|
|
Unary only: at most one data frame, an optional final trailer, exact frame flags. Only a
|
|
grpc-web-text response body is decoded; anything else is left unchanged.
|
|
"""
|
|
|
|
if not kb.grpcWeb:
|
|
return page
|
|
|
|
out = []
|
|
|
|
status = _headerValue(responseHeaders, "grpc-status")
|
|
if status is not None:
|
|
out.append("grpc-status:%s" % status)
|
|
message = _headerValue(responseHeaders, "grpc-message")
|
|
if message:
|
|
out.append("grpc-message:%s" % unquote(message))
|
|
|
|
if page and _isTextContentType(_headerValue(responseHeaders, HTTP_HEADER.CONTENT_TYPE)):
|
|
try:
|
|
raw = _b64decode(page)
|
|
bodyOut = [] # separate so a mid-parse failure never leaks partial frame renders
|
|
pos = 0
|
|
dataFrames = 0
|
|
seenTrailer = False
|
|
while pos + 5 <= len(raw):
|
|
flag = raw[pos] if isinstance(raw[pos], int) else ord(raw[pos])
|
|
length = struct.unpack(">I", raw[pos + 1:pos + 5])[0]
|
|
payload, pos = _readExact(raw, pos + 5, length)
|
|
if seenTrailer:
|
|
raise ValueError("frame after trailer")
|
|
if flag == 0x00: # data frame
|
|
dataFrames += 1
|
|
if dataFrames > 1:
|
|
raise ValueError("streaming response not supported")
|
|
for _fn, wt, val in _decode(payload):
|
|
bodyOut.append(bytes(val).decode("utf-8", "replace") if wt == 2 else str(val))
|
|
elif flag == 0x80: # trailer frame (grpc-status / grpc-message), must be last
|
|
seenTrailer = True
|
|
bodyOut.append(unquote(payload.decode("latin-1")))
|
|
else:
|
|
raise ValueError("unsupported response frame flags 0x%02x" % flag)
|
|
if pos != len(raw):
|
|
raise ValueError("trailing bytes after final frame")
|
|
out.extend(bodyOut) # commit body renders only on a fully-valid parse
|
|
except Exception:
|
|
# keep status/message recovered from HEADERS (discard any partial body); append the raw page
|
|
out.append(page)
|
|
return "\n".join(out)
|
|
elif page:
|
|
out.append(page) # unsupported/absent response Content-Type: leave the body for the oracle
|
|
|
|
return "\n".join(out) if out else page
|