|
| 1 | +#!/usr/bin/env python |
| 2 | + |
| 3 | +""" |
| 4 | +Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org) |
| 5 | +See the file 'LICENSE' for copying permission |
| 6 | +""" |
| 7 | + |
| 8 | +from lib.core.common import Backend |
| 9 | +from lib.core.common import popValue |
| 10 | +from lib.core.common import pushValue |
| 11 | +from lib.core.data import conf |
| 12 | +from lib.core.data import kb |
| 13 | +from lib.core.data import logger |
| 14 | +from lib.core.enums import DBMS |
| 15 | +from lib.request.inject import checkBooleanExpression |
| 16 | + |
| 17 | +# Operator-dialect probes for a keyword-free back-end DBMS heuristic. |
| 18 | +# |
| 19 | +# Each probe is an arithmetic identity that holds only in the dialect(s) noted, using operator |
| 20 | +# *semantics* alone - no SQL keywords, functions, quotes or schema names. It complements |
| 21 | +# heuristicCheckDbms() (which uses (SELECT 'x')='x' string round-trips): the dialect probes carry |
| 22 | +# no SELECT/quote, so they can narrow the back-end DBMS where those are dropped (e.g. a |
| 23 | +# keyword-matching WAF/IPS, or when kb.droppingRequests has it skipped entirely). |
| 24 | +# |
| 25 | +# Each probe is evaluated through checkBooleanExpression(), i.e. as an appended boolean |
| 26 | +# (... AND (<probe>)), which yields a clean true/false from the comparison oracle. (A value-position |
| 27 | +# variant - replacing the value with id=2^0 etc. - was prototyped and rejected: those probes land on |
| 28 | +# OTHER valid rows, which sqlmap's fuzzy page comparison conflates with the anchor row, producing |
| 29 | +# false positives. See PROVE_DESIGN.md.) |
| 30 | +# |
| 31 | +# Truth table measured on a live OWASP-CRS platform across 11 engines (MySQL, MariaDB/TiDB, |
| 32 | +# PostgreSQL, CockroachDB, Microsoft SQL Server, SQLite, Firebird, ClickHouse, H2, HSQLDB, Derby); |
| 33 | +# only the zero-false-positive rules are kept (see _classify). With anchor value 2: |
| 34 | +# |
| 35 | +# * 2^0=2 -> '^' is bitwise XOR (MySQL/MSSQL: 2^0=2) vs exponentiation (PostgreSQL: 2^0=1) vs |
| 36 | +# no such operator (SQLite/Oracle/... -> error, so false) |
| 37 | +# * 2^3=8 -> '^' is exponentiation (PostgreSQL/CockroachDB: 2^3=8) - false for XOR dialects |
| 38 | +# (2^3=1) and erroring dialects; a positive PostgreSQL-family marker. CAVEAT: |
| 39 | +# '^'=exponentiation is not strictly unique to PostgreSQL - MS Access/Jet and DuckDB |
| 40 | +# also use it (neither on the platform), so this can read as PostgreSQL there. |
| 41 | +# * 5/2=2 -> integer division (PostgreSQL/MSSQL/SQLite) vs real division (MySQL/Oracle: 2.5) |
| 42 | +# * 2|0=2 -> a bitwise OR operator exists (absent in Firebird/Oracle/ClickHouse/H2) |
| 43 | +DIALECT_PROBES = ( |
| 44 | + ("xor", "2^0=2"), |
| 45 | + ("pgpow", "2^3=8"), |
| 46 | + ("intdiv", "5/2=2"), |
| 47 | + ("bitor", "2|0=2"), |
| 48 | +) |
| 49 | + |
| 50 | +def _classify(signature): |
| 51 | + """ |
| 52 | + Maps a measured (xor, pgpow, intdiv, bitor) operator-dialect signature to a back-end |
| 53 | + DBMS, or returns None when the signature does not *uniquely* identify a major DBMS (so |
| 54 | + detection proceeds unchanged - the heuristic never wrong-foots the scan). |
| 55 | +
|
| 56 | + Rules below are the subset of the measured 11-engine truth table that maps with zero |
| 57 | + false positives. Engines whose operator profile is not distinctive enough (Oracle's |
| 58 | + all-false signature, which a minimal engine like ClickHouse/H2/Firebird/HSQLDB/Derby or |
| 59 | + a fully WAF-blocked channel also produces) deliberately fall through to None: |
| 60 | +
|
| 61 | + >>> _classify((True, False, False, True)) # MySQL / MariaDB / TiDB |
| 62 | + 'MySQL' |
| 63 | + >>> _classify((True, False, True, True)) # Microsoft SQL Server |
| 64 | + 'Microsoft SQL Server' |
| 65 | + >>> _classify((False, True, True, True)) # PostgreSQL |
| 66 | + 'PostgreSQL' |
| 67 | + >>> _classify((False, True, False, True)) # CockroachDB (pgwire) -> PostgreSQL family |
| 68 | + 'PostgreSQL' |
| 69 | + >>> _classify((False, False, True, True)) # SQLite |
| 70 | + 'SQLite' |
| 71 | + >>> _classify((False, False, True, False)) is None # Firebird/HSQLDB/Derby/H2 -> no prior |
| 72 | + True |
| 73 | + >>> _classify((False, False, False, False)) is None # all-false (Oracle/ClickHouse/blocked) -> no prior |
| 74 | + True |
| 75 | + """ |
| 76 | + |
| 77 | + xor, pgpow, intdiv, bitor = signature |
| 78 | + |
| 79 | + if pgpow: # '^' is exponentiation -> PostgreSQL family |
| 80 | + return DBMS.PGSQL |
| 81 | + if xor and intdiv: # '^' is XOR AND integer division -> SQL Server |
| 82 | + return DBMS.MSSQL |
| 83 | + if xor and not intdiv: # '^' is XOR AND real division -> MySQL family |
| 84 | + return DBMS.MYSQL |
| 85 | + if not xor and intdiv and bitor: # no '^', integer division, bitwise '|' -> SQLite |
| 86 | + return DBMS.SQLITE |
| 87 | + |
| 88 | + return None |
| 89 | + |
| 90 | +def dialectCheckDbms(injection): |
| 91 | + """ |
| 92 | + Keyword-free back-end DBMS heuristic via operator-dialect differentials, evaluated through the |
| 93 | + given (boolean-capable) injection. Complements heuristicCheckDbms() - which is skipped when the |
| 94 | + WAF/IPS is dropping requests and otherwise relies on SELECT/quote payloads - because every probe |
| 95 | + here is built from operator semantics alone. Returns the DBMS name or None; an ambiguous or |
| 96 | + WAF-blocked channel yields None, leaving the scan unchanged. |
| 97 | + """ |
| 98 | + |
| 99 | + retVal = None |
| 100 | + |
| 101 | + if conf.skipHeuristics: |
| 102 | + return retVal |
| 103 | + |
| 104 | + pushValue(kb.injection) |
| 105 | + kb.injection = injection |
| 106 | + |
| 107 | + try: |
| 108 | + # channel sanity: a tautology must read TRUE and a contradiction FALSE, otherwise the |
| 109 | + # boolean oracle is unreliable and the all-false signature (Oracle-like) would be meaningless |
| 110 | + if checkBooleanExpression("2=2") and not checkBooleanExpression("2=3"): |
| 111 | + signature = tuple(bool(checkBooleanExpression(expr)) for _, expr in DIALECT_PROBES) |
| 112 | + retVal = _classify(signature) |
| 113 | + finally: |
| 114 | + kb.injection = popValue() |
| 115 | + |
| 116 | + if retVal and not Backend.getIdentifiedDbms(): |
| 117 | + infoMsg = "heuristic (dialect) test shows that the back-end DBMS could be '%s'" % retVal |
| 118 | + logger.info(infoMsg) |
| 119 | + |
| 120 | + return retVal |
0 commit comments