diff --git a/lib/core/common.py b/lib/core/common.py index 65ce3450a9c..e0e83e7770d 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1714,7 +1714,7 @@ def parseTargetDirect(): try: conf.dbms = dbmsName - if dbmsName in (DBMS.ACCESS, DBMS.SQLITE, DBMS.FIREBIRD): + if dbmsName in (DBMS.ACCESS, DBMS.SQLITE): if remote: warnMsg = "direct connection over the network for " warnMsg += "%s DBMS is not supported" % dbmsName @@ -1749,7 +1749,7 @@ def parseTargetDirect(): elif dbmsName == DBMS.ACCESS: __import__("pyodbc") elif dbmsName == DBMS.FIREBIRD: - __import__("kinterbasdb") + __import__("firebirdsql") except (SqlmapSyntaxException, SqlmapMissingDependence): raise except: diff --git a/lib/core/dicts.py b/lib/core/dicts.py index 4abc2e8bfef..b2d4c7f94b8 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -231,8 +231,8 @@ DBMS.ORACLE: (ORACLE_ALIASES, "python-oracledb", "https://oracle.github.io/python-oracledb/", "oracle"), DBMS.SQLITE: (SQLITE_ALIASES, "python-sqlite", "https://docs.python.org/3/library/sqlite3.html", "sqlite"), DBMS.ACCESS: (ACCESS_ALIASES, "python-pyodbc", "https://github.com/mkleehammer/pyodbc", "access"), - DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python-kinterbasdb", "https://kinterbasdb.sourceforge.net/", "firebird"), - DBMS.MAXDB: (MAXDB_ALIASES, None, None, "maxdb"), + DBMS.FIREBIRD: (FIREBIRD_ALIASES, "python3-firebirdsql", "https://github.com/nakagami/pyfirebirdsql/", "firebird"), + DBMS.MAXDB: (MAXDB_ALIASES, None, None, None), # 'maxdb'/'sapdb' SQLAlchemy dialect was removed long ago; a dead dialect only produces a misleading warning DBMS.SYBASE: (SYBASE_ALIASES, "python-pymssql", "https://github.com/pymssql/pymssql", "sybase"), DBMS.DB2: (DB2_ALIASES, "python ibm-db", "https://github.com/ibmdb/python-ibmdb", "ibm_db_sa"), DBMS.HSQLDB: (HSQLDB_ALIASES, "python jaydebeapi & python-jpype", "https://pypi.python.org/pypi/JayDeBeApi/ & https://github.com/jpype-project/jpype", None), diff --git a/lib/core/settings.py b/lib/core/settings.py index e396b3782f3..fba21d81b44 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.77" +VERSION = "1.10.7.84" TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable" TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34} VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE) @@ -890,6 +890,10 @@ # the text extraction channel (mirrors a manual '--binary-fields', using the already-fetched column type) BINARY_FIELDS_TYPE_REGEX = r"(?i)binary|blob|bytea|image|\braw\b" +# Uppercased keywords of the above, for building an in-SQL "is this column binary-typed?" check when only +# column names (not types) were fetched - i.e. blind dumping (keep in sync with BINARY_FIELDS_TYPE_REGEX) +BINARY_FIELDS_TYPE_KEYWORDS = ("BINARY", "BLOB", "BYTEA", "IMAGE", "RAW") + # Maximum number of redirections to any single URL - this is needed because of the state that cookies introduce MAX_SINGLE_URL_REDIRECTIONS = 4 diff --git a/lib/request/direct.py b/lib/request/direct.py index 171f37151d6..52f410e9080 100644 --- a/lib/request/direct.py +++ b/lib/request/direct.py @@ -5,6 +5,7 @@ See the file 'LICENSE' for copying permission """ +import binascii import re import time @@ -29,6 +30,20 @@ from lib.utils.safe2bin import safecharencode from lib.utils.timeout import timeout +def _hexifyBinary(value): + """ + Renders a raw binary cell returned by a driver in -d mode as uppercase hex, instead of letting it reach + the text channel as a str() like '' (PostgreSQL bytea -> psycopg2 memoryview) or a + control-character blob that gets blanked/corrupted (e.g. MSSQL varbinary -> bytes). Text columns come back + as native strings, so only genuine binary values are converted (matches HEX()-based rendering elsewhere). + """ + + if isinstance(value, memoryview): + value = value.tobytes() + if isinstance(value, (bytes, bytearray)): + return getUnicode(binascii.hexlify(value)).upper() + return value + def direct(query, content=True): select = True query = agent.payloadDirect(query) @@ -45,7 +60,10 @@ def direct(query, content=True): break if select: - if re.search(r"(?i)\ASELECT ", query) is None: + # only auto-wrap a bare scalar expression (e.g. 'CURRENT_USER', '48*60') in SELECT; a user-supplied + # complete row-returning statement (--sql-query 'PRAGMA ...' / 'WITH ...' / 'EXPLAIN ...') must be left + # intact, otherwise 'SELECT PRAGMA ...' is a syntax error and silently returns nothing + if re.search(r"(?i)\A\s*(?:SELECT|WITH|PRAGMA|EXPLAIN|DESCRIBE|DESC|SHOW|TABLE|VALUES)\b", query) is None: query = "SELECT %s" % query if conf.binaryFields: @@ -63,9 +81,15 @@ def direct(query, content=True): timeout(func=conf.dbmsConnector.execute, args=(query,), duration=conf.timeout, default=None) elif not (output and ("%soutput" % conf.tablePrefix) not in query and ("%sfile" % conf.tablePrefix) not in query): output, state = timeout(func=conf.dbmsConnector.select, args=(query,), duration=conf.timeout, default=None) + if output and isListLike(output): + output = [tuple(_hexifyBinary(_) for _ in row) if isListLike(row) else _hexifyBinary(row) for row in output] if state == TIMEOUT_STATE.NORMAL: hashDBWrite(query, output, True) - elif state == TIMEOUT_STATE.TIMEOUT: + elif state in (TIMEOUT_STATE.TIMEOUT, TIMEOUT_STATE.EXCEPTION): + # a timed-out OR fatally-errored query (e.g. the connector raised SqlmapConnectionException, or the + # connection dropped mid-scan) left the connection unusable; reconnect so the rest of the scan + # recovers instead of every following query being silently swallowed to None (wrong/empty data). + # A genuinely dead DB makes connect() raise here and the scan aborts cleanly, as with TIMEOUT. conf.dbmsConnector.close() conf.dbmsConnector.connect() elif output: diff --git a/lib/utils/deps.py b/lib/utils/deps.py index 99fcc31e857..9bca3617c8c 100644 --- a/lib/utils/deps.py +++ b/lib/utils/deps.py @@ -38,7 +38,7 @@ def checkDependencies(): elif dbmsName == DBMS.ACCESS: __import__("pyodbc") elif dbmsName == DBMS.FIREBIRD: - __import__("kinterbasdb") + __import__("firebirdsql") elif dbmsName == DBMS.DB2: __import__("ibm_db_dbi") elif dbmsName in (DBMS.HSQLDB, DBMS.CACHE): diff --git a/lib/utils/prove.py b/lib/utils/prove.py index af11306c930..fccd275dc98 100644 --- a/lib/utils/prove.py +++ b/lib/utils/prove.py @@ -347,6 +347,10 @@ def proveExploitation(): # back, exploitation is NOT proven; say so plainly instead of echoing the detection verdict. proven = bool(rungs) + # whether ANY confirmed technique here can return data inline; a stacked-query-only point cannot, so a + # failed read-back below is expected there and must NOT be spun into a "false positive" verdict + canReadBack = any(_ in injection.data for _ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.TIME)) + if proven: if proof: fields.append(_field("Proof", proof)) @@ -361,7 +365,12 @@ def proveExploitation(): verdict = ["no value could be read back through the injection (tried a random arithmetic product and the DBMS banner)"] if suspectWaf: verdict.append("the TRUE/FALSE difference is only an HTTP %s (blocked) response - characteristic of a WAF/IPS, not a database answer" % signal.get("trueCode")) - if wafInterfering: + if not canReadBack: + # e.g. stacked-query-only: no confirmed technique returns data inline, so a value cannot be read + # back here - that is expected by design and is NOT evidence of a false positive + verdict.append("this injection point exposes no data-returning channel (e.g. stacked queries), so a value cannot be read back inline - expected here, not a false positive") + verdict.append("=> confirm exploitation through a side effect instead (e.g. '--os-shell', or '--sql-query' run with '--technique=S')") + elif wafInterfering: # behind a WAF, an unconfirmed read-back is ambiguous: a genuine injection whose data-retrieval # payloads are being blocked looks the same as a pure WAF artifact - so don't assert "false # positive", point the user at the way to disambiguate instead diff --git a/lib/utils/sqlalchemy.py b/lib/utils/sqlalchemy.py index d6d702ffc43..d079cfb3aab 100644 --- a/lib/utils/sqlalchemy.py +++ b/lib/utils/sqlalchemy.py @@ -117,7 +117,7 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False # Reference: https://stackoverflow.com/a/69491015 @@ -126,11 +126,24 @@ def execute(self, query): try: self.cursor = self.connector.execute(query) - if hasattr(self.connector, "commit"): # Note: SQLAlchemy 2.0+ dropped implicit autocommit (otherwise DML changes - e.g. via --sql-query - would be silently lost) + # Note: SQLAlchemy 2.0+ dropped implicit autocommit (otherwise DML changes - e.g. via --sql-query - + # would be silently lost). SELECT goes through select() with commit=False so the result set is + # fetched BEFORE committing: on some drivers (e.g. pymssql) commit() discards the open cursor, which + # otherwise made every MSSQL '-d' query silently return empty (banner/is-dba/dump all blank). + if commit and hasattr(self.connector, "commit"): self.connector.commit() retVal = True - except (_sqlalchemy.exc.OperationalError, _sqlalchemy.exc.ProgrammingError) as ex: + except (_sqlalchemy.exc.OperationalError, _sqlalchemy.exc.ProgrammingError, _sqlalchemy.exc.DataError, _sqlalchemy.exc.IntegrityError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + # Roll back the failed statement's transaction so it does not poison every following query with + # 'InFailedSqlTransaction' (SQLAlchemy 2.0+ keeps the transaction open after an error). Without this + # a single legitimately-failing probe - e.g. AURORA_VERSION() on vanilla PostgreSQL during + # fingerprinting - made all later queries silently return wrong values (e.g. '--is-dba' read False) + if hasattr(self.connector, "rollback"): + try: + self.connector.rollback() + except Exception: + pass except _sqlalchemy.exc.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) @@ -139,7 +152,14 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + # Fetch BEFORE committing (commit=False): committing can discard the open result cursor on some drivers + # (e.g. pymssql), which silently emptied every MSSQL '-d' result. No DML is persisted by a SELECT anyway. + if self.execute(query, commit=False): retVal = self.fetchall() + if hasattr(self.connector, "commit"): + try: + self.connector.commit() + except Exception: + pass return retVal diff --git a/plugins/dbms/access/connector.py b/plugins/dbms/access/connector.py index 91b8f246649..c1313bbf33d 100644 --- a/plugins/dbms/access/connector.py +++ b/plugins/dbms/access/connector.py @@ -38,7 +38,9 @@ def connect(self): self.checkFileDb() try: - self.connector = pyodbc.connect('Driver={Microsoft Access Driver (*.mdb)};Dbq=%s;Uid=Admin;Pwd=;' % self.db) + # ACE driver ('*.mdb, *.accdb') handles both legacy Jet .mdb and modern .accdb (the old '*.mdb'-only + # Jet driver is 32-bit-only and absent on modern installs); honor supplied credentials, not Admin/empty + self.connector = pyodbc.connect('Driver={Microsoft Access Driver (*.mdb, *.accdb)};Dbq=%s;Uid=%s;Pwd=%s;' % (self.db, self.user or "Admin", self.password or "")) except (pyodbc.Error, pyodbc.OperationalError) as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/cache/connector.py b/plugins/dbms/cache/connector.py index 67a661e4a4a..3ba07525be9 100644 --- a/plugins/dbms/cache/connector.py +++ b/plugins/dbms/cache/connector.py @@ -46,7 +46,7 @@ def connect(self): try: driver = 'com.intersys.jdbc.CacheDriver' connection_string = 'jdbc:Cache://%s:%d/%s' % (self.hostname, self.port, self.db) - self.connector = jaydebeapi.connect(driver, connection_string, str(self.user), str(self.password)) + self.connector = jaydebeapi.connect(driver, connection_string, [str(self.user), str(self.password)]) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/clickhouse/connector.py b/plugins/dbms/clickhouse/connector.py index 83a868de757..12d4987eefe 100755 --- a/plugins/dbms/clickhouse/connector.py +++ b/plugins/dbms/clickhouse/connector.py @@ -5,7 +5,51 @@ See the file 'LICENSE' for copying permission """ +try: + import clickhouse_connect + import clickhouse_connect.dbapi +except: + pass + +import logging + +from lib.core.common import getSafeExString +from lib.core.data import conf +from lib.core.data import logger +from lib.core.exception import SqlmapConnectionException from plugins.generic.connector import Connector as GenericConnector class Connector(GenericConnector): - pass + """ + Homepage: https://github.com/ClickHouse/clickhouse-connect + User guide: https://clickhouse.com/docs/integrations/python + License: Apache 2.0 + """ + + def connect(self): + self.initConnection() + + try: + self.connector = clickhouse_connect.dbapi.connect(host=self.hostname, port=self.port, username=self.user, password=self.password, database=self.db) + except clickhouse_connect.dbapi.Error as ex: + raise SqlmapConnectionException(getSafeExString(ex)) + + self.initCursor() + self.printConnected() + + def fetchall(self): + try: + return self.cursor.fetchall() + except clickhouse_connect.dbapi.Error as ex: + logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + return None + + def execute(self, query): + try: + self.cursor.execute(query) + except clickhouse_connect.dbapi.Error as ex: + logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) + + def select(self, query): + self.execute(query) + return self.fetchall() diff --git a/plugins/dbms/cubrid/connector.py b/plugins/dbms/cubrid/connector.py index 76aa9ea390c..cc17cd8aa1a 100644 --- a/plugins/dbms/cubrid/connector.py +++ b/plugins/dbms/cubrid/connector.py @@ -30,8 +30,10 @@ def connect(self): self.initConnection() try: - self.connector = CUBRIDdb.connect(hostname=self.hostname, username=self.user, password=self.password, database=self.db, port=self.port, connect_timeout=conf.timeout) - except CUBRIDdb.DatabaseError as ex: + # CUBRIDdb.connect takes a positional URL 'CUBRID:host:port:db:::' then user/password positionally + # (it does not accept hostname/username/database keyword args, which raised a TypeError before) + self.connector = CUBRIDdb.connect("CUBRID:%s:%s:%s:::" % (self.hostname, self.port, self.db), str(self.user), str(self.password)) + except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/db2/connector.py b/plugins/dbms/db2/connector.py index 0a8e96b7a34..7e30a336950 100644 --- a/plugins/dbms/db2/connector.py +++ b/plugins/dbms/db2/connector.py @@ -32,7 +32,7 @@ def connect(self): try: database = "DRIVER={IBM DB2 ODBC DRIVER};DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;" % (self.db, self.hostname, self.port) self.connector = ibm_db_dbi.connect(database, self.user, self.password) - except ibm_db_dbi.OperationalError as ex: + except ibm_db_dbi.Error as ex: # base class: ibm_db_dbi maps wrong-credential (SQLSTATE 28) to ProgrammingError, not OperationalError raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/derby/connector.py b/plugins/dbms/derby/connector.py index 7be45f7412b..1069977b203 100644 --- a/plugins/dbms/derby/connector.py +++ b/plugins/dbms/derby/connector.py @@ -30,7 +30,7 @@ def connect(self): self.initConnection() try: - self.connector = drda.connect(host=self.hostname, database=self.db, port=self.port) + self.connector = drda.connect(host=self.hostname, database=self.db, port=self.port, user=self.user or None, password=self.password or None) except drda.OperationalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/firebird/connector.py b/plugins/dbms/firebird/connector.py index 31a12b99d72..9e21a711fb0 100644 --- a/plugins/dbms/firebird/connector.py +++ b/plugins/dbms/firebird/connector.py @@ -6,7 +6,7 @@ """ try: - import kinterbasdb + import firebirdsql except: pass @@ -20,10 +20,12 @@ class Connector(GenericConnector): """ - Homepage: http://kinterbasdb.sourceforge.net/ - User guide: http://kinterbasdb.sourceforge.net/dist_docs/usage.html - Debian package: python-kinterbasdb + Homepage: https://github.com/nakagami/pyfirebirdsql + User guide: https://pyfirebirdsql.readthedocs.io/ + Debian package: python3-firebirdsql License: BSD + + Note: ported from the (Python 2-only, unmaintained) kinterbasdb driver to firebirdsql """ # sample usage: @@ -36,9 +38,8 @@ def connect(self): self.checkFileDb() try: - # Reference: http://www.daniweb.com/forums/thread248499.html - self.connector = kinterbasdb.connect(host=self.hostname, database=self.db, user=self.user, password=self.password, charset="UTF8") - except kinterbasdb.OperationalError as ex: + self.connector = firebirdsql.connect(host=self.hostname, database=self.db, port=self.port or 3050, user=self.user, password=self.password, charset="UTF8") + except firebirdsql.OperationalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() @@ -47,20 +48,25 @@ def connect(self): def fetchall(self): try: return self.cursor.fetchall() - except kinterbasdb.OperationalError as ex: + except firebirdsql.OperationalError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): try: self.cursor.execute(query) - except kinterbasdb.OperationalError as ex: + except firebirdsql.OperationalError as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) - except kinterbasdb.Error as ex: + except firebirdsql.Error as ex: raise SqlmapConnectionException(getSafeExString(ex)) - self.connector.commit() + # commit non-SELECT (DML) here; select() commits only AFTER fetchall() because a Firebird COMMIT closes + # open cursors (discarding an unfetched result set) + if commit: + self.connector.commit() def select(self, query): - self.execute(query) - return self.fetchall() + self.execute(query, commit=False) + retVal = self.fetchall() + self.connector.commit() + return retVal diff --git a/plugins/dbms/hsqldb/connector.py b/plugins/dbms/hsqldb/connector.py index 95630b76e6b..494c2988bbd 100644 --- a/plugins/dbms/hsqldb/connector.py +++ b/plugins/dbms/hsqldb/connector.py @@ -45,8 +45,8 @@ def connect(self): try: driver = 'org.hsqldb.jdbc.JDBCDriver' - connection_string = 'jdbc:hsqldb:mem:.' # 'jdbc:hsqldb:hsql://%s/%s' % (self.hostname, self.db) - self.connector = jaydebeapi.connect(driver, connection_string, str(self.user), str(self.password)) + connection_string = 'jdbc:hsqldb:hsql://%s:%s/%s' % (self.hostname, self.port, self.db) # was hardcoded to 'jdbc:hsqldb:mem:.' (a fresh empty in-memory DB), ignoring the -d target + self.connector = jaydebeapi.connect(driver, connection_string, [str(self.user), str(self.password)]) except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/hsqldb/filesystem.py b/plugins/dbms/hsqldb/filesystem.py index 869279e7e4a..2bd06696c50 100644 --- a/plugins/dbms/hsqldb/filesystem.py +++ b/plugins/dbms/hsqldb/filesystem.py @@ -52,7 +52,7 @@ def stackedWriteFile(self, localFile, remoteFile, fileType=None, forceCheck=Fals logger.debug("cleaning up the database management system") - delQuery = "DELETE PROCEDURE %s" % func_name + delQuery = "DROP PROCEDURE %s" % func_name inject.goStacked(delQuery) message = "the local file '%s' has been written on the back-end DBMS" % localFile diff --git a/plugins/dbms/informix/connector.py b/plugins/dbms/informix/connector.py index e6f05889c0f..b7ae9e80506 100644 --- a/plugins/dbms/informix/connector.py +++ b/plugins/dbms/informix/connector.py @@ -32,7 +32,7 @@ def connect(self): try: database = "DATABASE=%s;HOSTNAME=%s;PORT=%s;PROTOCOL=TCPIP;" % (self.db, self.hostname, self.port) self.connector = ibm_db_dbi.connect(database, self.user, self.password) - except ibm_db_dbi.OperationalError as ex: + except ibm_db_dbi.Error as ex: # base class: ibm_db_dbi maps wrong-credential (SQLSTATE 28) to ProgrammingError, not OperationalError raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/informix/fingerprint.py b/plugins/dbms/informix/fingerprint.py index f71e6deff80..9936a4deeec 100644 --- a/plugins/dbms/informix/fingerprint.py +++ b/plugins/dbms/informix/fingerprint.py @@ -97,7 +97,7 @@ def checkDbms(self): logger.info(infoMsg) for version in ("14.1", "12.1", "11.7", "11.5", "10.0"): - output = inject.checkBooleanExpression("EXISTS(SELECT 1 FROM SYSMASTER:SYSDUAL WHERE DBINFO('VERSION,'FULL') LIKE '%%%s%%')" % version) + output = inject.checkBooleanExpression("EXISTS(SELECT 1 FROM SYSMASTER:SYSDUAL WHERE DBINFO('VERSION','FULL') LIKE '%%%s%%')" % version) if output: Backend.setVersion(version) diff --git a/plugins/dbms/mimersql/connector.py b/plugins/dbms/mimersql/connector.py index e6bcced6b96..74d27c43706 100644 --- a/plugins/dbms/mimersql/connector.py +++ b/plugins/dbms/mimersql/connector.py @@ -30,8 +30,10 @@ def connect(self): self.initConnection() try: - self.connector = mimerpy.connect(hostname=self.hostname, username=self.user, password=self.password, database=self.db, port=self.port, connect_timeout=conf.timeout) - except mimerpy.OperationalError as ex: + # mimerpy.connect uses dsn/user/password (host/port come from Mimer's sqlhosts/MIMER_DATABASE + # configuration, not connect() kwargs); the previous hostname/username/... kwargs raised a TypeError + self.connector = mimerpy.connect(dsn=self.db, user=str(self.user), password=str(self.password)) + except Exception as ex: raise SqlmapConnectionException(getSafeExString(ex)) self.initCursor() diff --git a/plugins/dbms/mssqlserver/connector.py b/plugins/dbms/mssqlserver/connector.py index f49cabaa646..fbc57a53255 100644 --- a/plugins/dbms/mssqlserver/connector.py +++ b/plugins/dbms/mssqlserver/connector.py @@ -54,11 +54,20 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False try: self.cursor.execute(getText(query)) + # Commit non-SELECT (DML/DDL) here: direct() routes those to execute() alone, so without this a + # '--sql-query'/'--sql-shell' write was silently rolled back on connection close. select() passes + # commit=False and commits only AFTER fetchall(), because on pymssql commit() discards the open + # result cursor (which otherwise emptied every SELECT result). + if commit: + try: + self.connector.commit() + except pymssql.OperationalError: + pass retVal = True except (pymssql.OperationalError, pymssql.ProgrammingError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) @@ -70,7 +79,7 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + if self.execute(query, commit=False): retVal = self.fetchall() try: diff --git a/plugins/dbms/mysql/connector.py b/plugins/dbms/mysql/connector.py index 459ff23d5bc..bfa87d4239c 100644 --- a/plugins/dbms/mysql/connector.py +++ b/plugins/dbms/mysql/connector.py @@ -12,7 +12,6 @@ import logging import struct -import sys from lib.core.common import getSafeExString from lib.core.data import conf @@ -34,7 +33,7 @@ def connect(self): self.initConnection() try: - self.connector = pymysql.connect(host=self.hostname, user=self.user, passwd=self.password.encode(sys.stdin.encoding), db=self.db, port=self.port, connect_timeout=conf.timeout, use_unicode=True) + self.connector = pymysql.connect(host=self.hostname, user=self.user, passwd=self.password, db=self.db, port=self.port, connect_timeout=conf.timeout, use_unicode=True) except (pymysql.OperationalError, pymysql.InternalError, pymysql.ProgrammingError, struct.error) as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/oracle/connector.py b/plugins/dbms/oracle/connector.py index 0d011fb8afd..550a413055c 100644 --- a/plugins/dbms/oracle/connector.py +++ b/plugins/dbms/oracle/connector.py @@ -32,6 +32,14 @@ class Connector(GenericConnector): def connect(self): self.initConnection() + # Fetch CLOB/BLOB values directly as str/bytes instead of oracledb.LOB objects; otherwise a LOB cell + # reached the renderer as the repr '' (BLOB bytes are then hex-encoded + # by direct()'s binary handling). + try: + oracledb.defaults.fetch_lobs = False + except AttributeError: + pass + self.user = getText(self.user) self.password = getText(self.password) diff --git a/plugins/dbms/postgresql/connector.py b/plugins/dbms/postgresql/connector.py index 4a71bf15bb6..33923e8f1d5 100644 --- a/plugins/dbms/postgresql/connector.py +++ b/plugins/dbms/postgresql/connector.py @@ -55,7 +55,10 @@ def execute(self, query): try: self.cursor.execute(query) retVal = True - except (psycopg2.OperationalError, psycopg2.ProgrammingError) as ex: + # Note: also catch DataError/IntegrityError (e.g. division-by-zero, bad cast, unique violation from a + # user '--sql-query') so the commit() below still runs and clears the aborted transaction; otherwise + # PostgreSQL poisons every later query with 'InFailedSqlTransaction' and silently returns None + except (psycopg2.OperationalError, psycopg2.ProgrammingError, psycopg2.DataError, psycopg2.IntegrityError) as ex: logger.warning(("(remote) '%s'" % getSafeExString(ex)).strip()) except psycopg2.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) diff --git a/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py index ea187fc79b0..ee9b70f345e 100644 --- a/plugins/dbms/postgresql/takeover.py +++ b/plugins/dbms/postgresql/takeover.py @@ -17,6 +17,7 @@ from lib.core.common import isStackingAvailable from lib.core.common import randomStr from lib.core.compat import LooseVersion +from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths @@ -100,7 +101,7 @@ def uncPathRequest(self): def copyExecCmd(self, cmd): output = None - if isStackingAvailable(): + if isStackingAvailable() or conf.direct: # Reference: https://medium.com/greenwolf-security/authenticated-arbitrary-command-execution-on-postgresql-9-3-latest-cd18945914d5 self._forgedCmd = "DROP TABLE IF EXISTS %s;" % self.cmdTblName self._forgedCmd += "CREATE TABLE %s(%s text);" % (self.cmdTblName, self.tblField) diff --git a/plugins/dbms/snowflake/connector.py b/plugins/dbms/snowflake/connector.py index c24f3ab17b6..cad4fd3a932 100644 --- a/plugins/dbms/snowflake/connector.py +++ b/plugins/dbms/snowflake/connector.py @@ -32,6 +32,14 @@ def __init__(self): def connect(self): self.initConnection() + # Snowflake's mandatory 'account' identifier is carried in the DSN host field + # (e.g. -d "snowflake://user:pass@ACCOUNT/db"); warehouse/schema are optional. These were previously + # read from self.account/self.warehouse/self.schema which were never set anywhere -> AttributeError on + # every attempt. + self.account = self.hostname + self.warehouse = getattr(self, "warehouse", None) + self.schema = getattr(self, "schema", None) + try: self.connector = snowflake.connector.connect( user=self.user, diff --git a/plugins/dbms/sybase/connector.py b/plugins/dbms/sybase/connector.py index aed2d79e393..308f9081217 100644 --- a/plugins/dbms/sybase/connector.py +++ b/plugins/dbms/sybase/connector.py @@ -54,11 +54,20 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) return None - def execute(self, query): + def execute(self, query, commit=True): retVal = False try: self.cursor.execute(getText(query)) + # Commit non-SELECT (DML/DDL) here: direct() routes those to execute() alone, so without this a + # '--sql-query'/'--sql-shell' write was silently rolled back on connection close. select() passes + # commit=False and commits only AFTER fetchall(), because on pymssql commit() discards the open + # result cursor (which otherwise emptied every SELECT result). + if commit: + try: + self.connector.commit() + except pymssql.OperationalError: + pass retVal = True except (pymssql.OperationalError, pymssql.ProgrammingError) as ex: logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) '%s'" % getSafeExString(ex).replace("\n", " ")) @@ -70,7 +79,7 @@ def execute(self, query): def select(self, query): retVal = None - if self.execute(query): + if self.execute(query, commit=False): retVal = self.fetchall() try: diff --git a/plugins/dbms/vertica/connector.py b/plugins/dbms/vertica/connector.py index bfce0ce645d..6809989369c 100644 --- a/plugins/dbms/vertica/connector.py +++ b/plugins/dbms/vertica/connector.py @@ -44,7 +44,7 @@ def fetchall(self): logger.log(logging.WARN if conf.dbmsHandler else logging.DEBUG, "(remote) %s" % getSafeExString(ex)) return None - def execute(self, query): + def execute(self, query, commit=True): try: self.cursor.execute(query) except (vertica_python.OperationalError, vertica_python.ProgrammingError) as ex: @@ -52,8 +52,13 @@ def execute(self, query): except vertica_python.InternalError as ex: raise SqlmapConnectionException(getSafeExString(ex)) - self.connector.commit() + # commit non-SELECT (DML) here; select() commits only AFTER fetchall() because vertica_python shares one + # cursor per connection and commit() runs COMMIT through it, discarding the unfetched result set + if commit: + self.connector.commit() def select(self, query): - self.execute(query) - return self.fetchall() + self.execute(query, commit=False) + retVal = self.fetchall() + self.connector.commit() + return retVal diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index 7b50b634460..b648714bdef 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -46,6 +46,7 @@ from lib.core.exception import SqlmapMissingMandatoryOptionException from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapUserQuitException +from lib.core.settings import BINARY_FIELDS_TYPE_KEYWORDS from lib.core.settings import CURRENT_DB from lib.core.settings import METADB_SUFFIX from lib.core.settings import PLUS_ONE_DBMSES @@ -931,7 +932,16 @@ def columnNameQuery(index): warnMsg += "possible to get column comments" singleTimeWarnMessage(warnMsg) - if not onlyColNames: + # In dump mode we don't need the exact type, only whether the column is binary (so its raw + # bytes get hex-extracted instead of mangled/truncated - issues #8/#582/#2827). Rather than + # extract the whole type string char-by-char, wrap the type query into a single-bit check and + # extract just that (~10x fewer requests). Skipped where query2 doesn't yield a type name: + # MSSQL (returns the column name), Firebird/Informix (numeric type codes), and PostgreSQL + # (its bytea already renders fine, so it's excluded from auto-hexing anyway). + binaryProbe = onlyColNames and dumpMode and not Backend.getIdentifiedDbms() in (DBMS.MSSQL, DBMS.PGSQL, DBMS.FIREBIRD, DBMS.INFORMIX) + + if not onlyColNames or binaryProbe: + query = None if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2, DBMS.VERTICA, DBMS.PRESTO, DBMS.CRATEDB, DBMS.CACHE, DBMS.FRONTBASE, DBMS.VIRTUOSO, DBMS.CLICKHOUSE): query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column, unsafeSQLIdentificatorNaming(conf.db)) elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.DERBY, DBMS.ALTIBASE, DBMS.MIMERSQL): @@ -947,22 +957,35 @@ def columnNameQuery(index): elif Backend.isDbms(DBMS.SPANNER): query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column, unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(conf.db)) - colType = unArrayizeValue(inject.getValue(query, union=False, error=False)) - key = int(colType) if hasattr(colType, "isdigit") and colType.isdigit() else colType + if binaryProbe and query: + typeMatch = re.match(r"(?is)\s*SELECT\s+(.+?)\s+FROM\s+(.+)", query) + if typeMatch: + binaryCondition = " OR ".join("UPPER(%s) LIKE '%%%s%%'" % (typeMatch.group(1), _) for _ in BINARY_FIELDS_TYPE_KEYWORDS) + query = "SELECT (CASE WHEN %s THEN 1 ELSE 0 END) FROM %s" % (binaryCondition, typeMatch.group(2)) + else: + query = None # unexpected shape - fall back to leaving the type unknown - if Backend.isDbms(DBMS.FIREBIRD): - colType = FIREBIRD_TYPES.get(key, colType) - elif Backend.isDbms(DBMS.INFORMIX): - notNull = False - if isinstance(key, int) and key > 255: - key -= 256 - notNull = True - colType = INFORMIX_TYPES.get(key, colType) - if notNull: - colType = "%s NOT NULL" % colType + colType = unArrayizeValue(inject.getValue(query, union=False, error=False)) if query else None - column = safeSQLIdentificatorNaming(column) - columns[column] = colType + if binaryProbe: + column = safeSQLIdentificatorNaming(column) + columns[column] = "binary" if colType in ('1', 1) else None # sentinel matched by BINARY_FIELDS_TYPE_REGEX + else: + key = int(colType) if hasattr(colType, "isdigit") and colType.isdigit() else colType + + if Backend.isDbms(DBMS.FIREBIRD): + colType = FIREBIRD_TYPES.get(key, colType) + elif Backend.isDbms(DBMS.INFORMIX): + notNull = False + if isinstance(key, int) and key > 255: + key -= 256 + notNull = True + colType = INFORMIX_TYPES.get(key, colType) + if notNull: + colType = "%s NOT NULL" % colType + + column = safeSQLIdentificatorNaming(column) + columns[column] = colType else: column = safeSQLIdentificatorNaming(column) columns[column] = None diff --git a/tamper/space2mssqlhash.py b/tamper/space2mssqlhash.py index befd6966ee0..880fdb8f954 100644 --- a/tamper/space2mssqlhash.py +++ b/tamper/space2mssqlhash.py @@ -15,11 +15,12 @@ def tamper(payload, **kwargs): Replaces space character (' ') with a pound character ('#') followed by a new line ('\n') Requirement: - * MSSQL * MySQL Notes: * Useful to bypass several web application firewalls + * The '#' single-line comment used here is MySQL-only (despite this script's legacy name); + T-SQL has no '#' comment, so it does not apply to Microsoft SQL Server >>> tamper('1 AND 9227=9227') '1%23%0AAND%23%0A9227=9227' diff --git a/tests/test_deps.py b/tests/test_deps.py index 5dfab5b50d5..c3224bd12e0 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -53,11 +53,11 @@ def tearDown(self): deps.logger = self._real_logger def test_missing_driver_warns_with_library_name(self): - # 'kinterbasdb' (Firebird driver) is essentially never installed, so the - # probe must hit the except branch and emit a warning naming the library. + # 'CUBRIDdb' (CUBRID driver) is not on PyPI and essentially never installed, + # so the probe must hit the except branch and emit a warning naming the library. try: - __import__("kinterbasdb") - self.skipTest("kinterbasdb is unexpectedly installed") + __import__("CUBRIDdb") + self.skipTest("CUBRIDdb is unexpectedly installed") except ImportError: pass @@ -65,10 +65,10 @@ def test_missing_driver_warns_with_library_name(self): warnings = self.rec.messages("warning") self.assertTrue(warnings, msg="no warnings captured for a missing driver") - # the Firebird entry must name its third-party library in a warning + # the CUBRID entry must name its third-party library in a warning self.assertTrue( - any("kinterbasdb" in w for w in warnings), - msg="missing Firebird driver did not produce a library-naming warning: %r" % warnings, + any("CUBRID-Python" in w for w in warnings), + msg="missing CUBRID driver did not produce a library-naming warning: %r" % warnings, ) def test_all_present_emits_all_installed_info(self):