Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions lib/core/dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
6 changes: 5 additions & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
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)
Expand Down Expand Up @@ -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

Expand Down
28 changes: 26 additions & 2 deletions lib/request/direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
See the file 'LICENSE' for copying permission
"""

import binascii
import re
import time

Expand All @@ -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 '<memory at 0x...>' (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)
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
11 changes: 10 additions & 1 deletion lib/utils/prove.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
28 changes: 24 additions & 4 deletions lib/utils/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))

Expand All @@ -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
4 changes: 3 additions & 1 deletion plugins/dbms/access/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/cache/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
46 changes: 45 additions & 1 deletion plugins/dbms/clickhouse/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
6 changes: 4 additions & 2 deletions plugins/dbms/cubrid/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/db2/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/derby/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
34 changes: 20 additions & 14 deletions plugins/dbms/firebird/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

try:
import kinterbasdb
import firebirdsql
except:
pass

Expand All @@ -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:
Expand All @@ -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()
Expand All @@ -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
4 changes: 2 additions & 2 deletions plugins/dbms/hsqldb/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/hsqldb/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/informix/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading