sqlmap/plugins/generic/databases.py

944 lines
45 KiB
Python
Raw Normal View History

#!/usr/bin/env python
2012-07-20 22:17:35 +04:00
"""
2019-01-05 23:38:52 +03:00
Copyright (c) 2006-2019 sqlmap developers (http://sqlmap.org/)
2017-10-11 15:50:46 +03:00
See the file 'LICENSE' for copying permission
2012-07-20 22:17:35 +04:00
"""
from lib.core.agent import agent
from lib.core.common import arrayizeValue
from lib.core.common import Backend
from lib.core.common import extractRegexResult
2012-07-20 22:17:35 +04:00
from lib.core.common import filterPairValues
2014-05-17 17:00:09 +04:00
from lib.core.common import flattenValue
2012-07-20 22:17:35 +04:00
from lib.core.common import getLimitRange
from lib.core.common import isInferenceAvailable
from lib.core.common import isListLike
from lib.core.common import isNoneValue
from lib.core.common import isNumPosStrValue
from lib.core.common import isTechniqueAvailable
from lib.core.common import parseSqliteTableSchema
from lib.core.common import popValue
from lib.core.common import pushValue
from lib.core.common import randomStr
2012-07-20 22:17:35 +04:00
from lib.core.common import readInput
from lib.core.common import safeSQLIdentificatorNaming
2018-02-13 17:53:50 +03:00
from lib.core.common import singleTimeLogMessage
from lib.core.common import singleTimeWarnMessage
2012-07-20 22:17:35 +04:00
from lib.core.common import unArrayizeValue
from lib.core.common import unsafeSQLIdentificatorNaming
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.data import paths
from lib.core.data import queries
from lib.core.decorators import stackedmethod
2012-08-21 13:30:01 +04:00
from lib.core.dicts import FIREBIRD_TYPES
2016-09-23 19:03:31 +03:00
from lib.core.dicts import INFORMIX_TYPES
2012-07-20 22:17:35 +04:00
from lib.core.enums import CHARSET_TYPE
from lib.core.enums import DBMS
from lib.core.enums import EXPECTED
from lib.core.enums import PAYLOAD
from lib.core.exception import SqlmapMissingMandatoryOptionException
from lib.core.exception import SqlmapNoneDataException
from lib.core.exception import SqlmapUserQuitException
2012-07-20 22:17:35 +04:00
from lib.core.settings import CURRENT_DB
from lib.request import inject
from lib.techniques.union.use import unionUse
2017-04-18 14:53:41 +03:00
from lib.utils.brute import columnExists
from lib.utils.brute import tableExists
2012-07-20 22:17:35 +04:00
class Databases:
"""
This class defines databases' enumeration functionalities for plugins.
"""
def __init__(self):
kb.data.currentDb = ""
kb.data.cachedDbs = []
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
kb.data.cachedCounts = {}
kb.data.dumpedTable = {}
def getCurrentDb(self):
infoMsg = "fetching current database"
logger.info(infoMsg)
query = queries[Backend.getIdentifiedDbms()].current_db.query
if not kb.data.currentDb:
kb.data.currentDb = unArrayizeValue(inject.getValue(query, safeCharEncode=False))
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL):
warnMsg = "on %s you'll need to use " % Backend.getIdentifiedDbms()
warnMsg += "schema names for enumeration as the counterpart to database "
warnMsg += "names on other DBMSes"
singleTimeWarnMessage(warnMsg)
2012-07-20 22:17:35 +04:00
return kb.data.currentDb
def getDbs(self):
if len(kb.data.cachedDbs) > 0:
return kb.data.cachedDbs
infoMsg = None
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
warnMsg = "information_schema not available, "
warnMsg += "back-end DBMS is MySQL < 5. database "
warnMsg += "names will be fetched from 'mysql' database"
logger.warn(warnMsg)
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.PGSQL):
warnMsg = "schema names are going to be used on %s " % Backend.getIdentifiedDbms()
2012-07-20 22:17:35 +04:00
warnMsg += "for enumeration as the counterpart to database "
warnMsg += "names on other DBMSes"
logger.warn(warnMsg)
infoMsg = "fetching database (schema) names"
else:
infoMsg = "fetching database names"
if infoMsg:
logger.info(infoMsg)
rootQuery = queries[Backend.getIdentifiedDbms()].dbs
2012-12-05 13:45:17 +04:00
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
2012-07-20 22:17:35 +04:00
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
query = rootQuery.inband.query2
else:
query = rootQuery.inband.query
values = inject.getValue(query, blind=False, time=False)
2012-07-20 22:17:35 +04:00
if not isNoneValue(values):
kb.data.cachedDbs = arrayizeValue(values)
2012-07-20 22:17:35 +04:00
if not kb.data.cachedDbs and isInferenceAvailable() and not conf.direct:
infoMsg = "fetching number of databases"
logger.info(infoMsg)
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
query = rootQuery.blind.count2
else:
query = rootQuery.blind.count
count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
2012-07-20 22:17:35 +04:00
if not isNumPosStrValue(count):
errMsg = "unable to retrieve the number of databases"
logger.error(errMsg)
else:
plusOne = Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2)
indexRange = getLimitRange(count, plusOne=plusOne)
for index in indexRange:
if Backend.isDbms(DBMS.SYBASE):
query = rootQuery.blind.query % (kb.data.cachedDbs[-1] if kb.data.cachedDbs else " ")
elif Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
query = rootQuery.blind.query2 % index
else:
query = rootQuery.blind.query % index
db = unArrayizeValue(inject.getValue(query, union=False, error=False))
2012-07-20 22:17:35 +04:00
if db:
kb.data.cachedDbs.append(safeSQLIdentificatorNaming(db))
if not kb.data.cachedDbs and Backend.isDbms(DBMS.MSSQL):
2012-12-05 13:45:17 +04:00
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
2012-07-20 22:17:35 +04:00
blinds = (False, True)
else:
blinds = (True,)
for blind in blinds:
count = 0
kb.data.cachedDbs = []
while True:
query = rootQuery.inband.query2 % count
value = unArrayizeValue(inject.getValue(query, blind=blind))
if not (value or "").strip():
2012-07-20 22:17:35 +04:00
break
else:
kb.data.cachedDbs.append(value)
2012-07-20 22:17:35 +04:00
count += 1
if kb.data.cachedDbs:
break
if not kb.data.cachedDbs:
infoMsg = "falling back to current database"
logger.info(infoMsg)
self.getCurrentDb()
if kb.data.currentDb:
kb.data.cachedDbs = [kb.data.currentDb]
else:
errMsg = "unable to retrieve the database names"
raise SqlmapNoneDataException(errMsg)
2012-07-20 22:17:35 +04:00
else:
kb.data.cachedDbs.sort()
2012-12-18 12:55:33 +04:00
if kb.data.cachedDbs:
2014-05-20 15:44:10 +04:00
kb.data.cachedDbs = filter(None, list(set(flattenValue(kb.data.cachedDbs))))
2012-12-18 12:55:33 +04:00
2012-07-20 22:17:35 +04:00
return kb.data.cachedDbs
def getTables(self, bruteForce=None):
if len(kb.data.cachedTables) > 0:
return kb.data.cachedTables
self.forceDbmsEnum()
if bruteForce is None:
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
errMsg = "information_schema not available, "
errMsg += "back-end DBMS is MySQL < 5.0"
logger.error(errMsg)
bruteForce = True
elif Backend.isDbms(DBMS.ACCESS):
try:
tables = self.getTables(False)
except SqlmapNoneDataException:
2012-07-20 22:17:35 +04:00
tables = None
if not tables:
errMsg = "cannot retrieve table names, "
errMsg += "back-end DBMS is Access"
logger.error(errMsg)
bruteForce = True
else:
return tables
if conf.db == CURRENT_DB:
conf.db = self.getCurrentDb()
if conf.db and Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.HSQLDB):
2012-07-20 22:17:35 +04:00
conf.db = conf.db.upper()
if conf.db:
2017-04-18 16:56:24 +03:00
dbs = conf.db.split(',')
2012-07-20 22:17:35 +04:00
else:
dbs = self.getDbs()
2014-06-22 02:19:10 +04:00
dbs = [_ for _ in dbs if _ and _.strip()]
2012-07-20 22:17:35 +04:00
for db in dbs:
dbs[dbs.index(db)] = safeSQLIdentificatorNaming(db)
if bruteForce:
resumeAvailable = False
for db, table in kb.brute.tables:
if db == conf.db:
resumeAvailable = True
break
2014-02-05 18:11:39 +04:00
if resumeAvailable and not conf.freshQueries:
2012-07-20 22:17:35 +04:00
for db, table in kb.brute.tables:
if db == conf.db:
if conf.db not in kb.data.cachedTables:
kb.data.cachedTables[conf.db] = [table]
else:
kb.data.cachedTables[conf.db].append(table)
return kb.data.cachedTables
2015-12-29 15:19:25 +03:00
message = "do you want to use common table existence check? %s " % ("[Y/n/q]" if Backend.getIdentifiedDbms() in (DBMS.ACCESS,) else "[y/N/q]")
2017-04-19 15:46:27 +03:00
choice = readInput(message, default='Y' if 'Y' in message else 'N').upper()
2012-07-20 22:17:35 +04:00
2017-04-18 16:48:05 +03:00
if choice == 'N':
2012-07-20 22:17:35 +04:00
return
2017-04-18 16:48:05 +03:00
elif choice == 'Q':
raise SqlmapUserQuitException
2012-07-20 22:17:35 +04:00
else:
return tableExists(paths.COMMON_TABLES)
infoMsg = "fetching tables for database"
2013-02-15 19:48:58 +04:00
infoMsg += "%s: '%s'" % ("s" if len(dbs) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(unArrayizeValue(db)) for db in sorted(dbs)))
2012-07-20 22:17:35 +04:00
logger.info(infoMsg)
rootQuery = queries[Backend.getIdentifiedDbms()].tables
2012-12-05 13:45:17 +04:00
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
values = []
2012-07-20 22:17:35 +04:00
for query, condition in ((rootQuery.inband.query, getattr(rootQuery.inband, "condition", None)), (getattr(rootQuery.inband, "query2", None), getattr(rootQuery.inband, "condition2", None))):
if not isNoneValue(values) or not query:
break
2012-12-17 18:07:28 +04:00
if condition:
if not Backend.isDbms(DBMS.SQLITE):
query += " WHERE %s" % condition
2012-07-20 22:17:35 +04:00
if conf.excludeSysDbs:
infoMsg = "skipping system database%s '%s'" % ("s" if len(self.excludeDbsList) > 1 else "", ", ".join(unsafeSQLIdentificatorNaming(db) for db in self.excludeDbsList))
logger.info(infoMsg)
query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs) if db not in self.excludeDbsList)
else:
query += " IN (%s)" % ','.join("'%s'" % unsafeSQLIdentificatorNaming(db) for db in sorted(dbs))
2012-07-20 22:17:35 +04:00
if len(dbs) < 2 and ("%s," % condition) in query:
query = query.replace("%s," % condition, "", 1)
if query:
values = inject.getValue(query, blind=False, time=False)
2012-07-20 22:17:35 +04:00
if not isNoneValue(values):
values = filter(None, arrayizeValue(values))
2012-07-20 22:17:35 +04:00
if len(values) > 0 and not isListLike(values[0]):
2013-01-11 14:17:41 +04:00
values = [(dbs[0], _) for _ in values]
2012-07-20 22:17:35 +04:00
for db, table in filterPairValues(values):
2012-07-20 22:17:35 +04:00
db = safeSQLIdentificatorNaming(db)
2014-01-15 13:29:53 +04:00
table = safeSQLIdentificatorNaming(unArrayizeValue(table), True)
2012-07-20 22:17:35 +04:00
2018-05-22 00:44:21 +03:00
if conf.getComments:
_ = queries[Backend.getIdentifiedDbms()].table_comment
if hasattr(_, "query"):
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
query = _.query % (unsafeSQLIdentificatorNaming(db.upper()), unsafeSQLIdentificatorNaming(table.upper()))
else:
query = _.query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(table))
comment = unArrayizeValue(inject.getValue(query, blind=False, time=False))
if not isNoneValue(comment):
infoMsg = "retrieved comment '%s' for table '%s' " % (comment, unsafeSQLIdentificatorNaming(table))
infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(db)
logger.info(infoMsg)
else:
warnMsg = "on %s it is not " % Backend.getIdentifiedDbms()
warnMsg += "possible to get column comments"
singleTimeWarnMessage(warnMsg)
2012-07-20 22:17:35 +04:00
if db not in kb.data.cachedTables:
kb.data.cachedTables[db] = [table]
else:
kb.data.cachedTables[db].append(table)
if not kb.data.cachedTables and isInferenceAvailable() and not conf.direct:
for db in dbs:
if conf.excludeSysDbs and db in self.excludeDbsList:
2013-02-15 19:48:58 +04:00
infoMsg = "skipping system database '%s'" % unsafeSQLIdentificatorNaming(db)
2012-07-20 22:17:35 +04:00
logger.info(infoMsg)
2018-02-13 17:53:50 +03:00
continue
2012-07-20 22:17:35 +04:00
2018-02-13 17:53:50 +03:00
if conf.exclude and db in conf.exclude.split(','):
infoMsg = "skipping database '%s'" % unsafeSQLIdentificatorNaming(db)
singleTimeLogMessage(infoMsg)
2012-07-20 22:17:35 +04:00
continue
infoMsg = "fetching number of tables for "
infoMsg += "database '%s'" % unsafeSQLIdentificatorNaming(db)
logger.info(infoMsg)
if Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD, DBMS.MAXDB, DBMS.ACCESS):
query = rootQuery.blind.count
else:
query = rootQuery.blind.count % unsafeSQLIdentificatorNaming(db)
count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
2012-07-20 22:17:35 +04:00
2012-10-23 12:33:30 +04:00
if count == 0:
warnMsg = "database '%s' " % unsafeSQLIdentificatorNaming(db)
warnMsg += "appears to be empty"
logger.warn(warnMsg)
continue
elif not isNumPosStrValue(count):
2012-07-20 22:17:35 +04:00
warnMsg = "unable to retrieve the number of "
warnMsg += "tables for database '%s'" % unsafeSQLIdentificatorNaming(db)
logger.warn(warnMsg)
continue
tables = []
plusOne = Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2)
indexRange = getLimitRange(count, plusOne=plusOne)
for index in indexRange:
if Backend.isDbms(DBMS.SYBASE):
query = rootQuery.blind.query % (db, (kb.data.cachedTables[-1] if kb.data.cachedTables else " "))
elif Backend.getIdentifiedDbms() in (DBMS.MAXDB, DBMS.ACCESS):
query = rootQuery.blind.query % (kb.data.cachedTables[-1] if kb.data.cachedTables else " ")
elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD):
query = rootQuery.blind.query % index
elif Backend.getIdentifiedDbms() in (DBMS.HSQLDB, DBMS.INFORMIX):
2014-03-07 18:57:41 +04:00
query = rootQuery.blind.query % (index, unsafeSQLIdentificatorNaming(db))
2012-07-20 22:17:35 +04:00
else:
query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(db), index)
table = unArrayizeValue(inject.getValue(query, union=False, error=False))
2012-07-20 22:17:35 +04:00
if not isNoneValue(table):
kb.hintValue = table
table = safeSQLIdentificatorNaming(table, True)
tables.append(table)
2018-05-22 00:44:21 +03:00
if conf.getComments:
_ = queries[Backend.getIdentifiedDbms()].table_comment
if hasattr(_, "query"):
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
query = _.query % (unsafeSQLIdentificatorNaming(db.upper()), unsafeSQLIdentificatorNaming(table.upper()))
else:
query = _.query % (unsafeSQLIdentificatorNaming(db), unsafeSQLIdentificatorNaming(table))
comment = unArrayizeValue(inject.getValue(query, union=False, error=False))
if not isNoneValue(comment):
infoMsg = "retrieved comment '%s' for table '%s' " % (comment, unsafeSQLIdentificatorNaming(table))
infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(db)
logger.info(infoMsg)
else:
warnMsg = "on %s it is not " % Backend.getIdentifiedDbms()
warnMsg += "possible to get column comments"
singleTimeWarnMessage(warnMsg)
2012-07-20 22:17:35 +04:00
if tables:
kb.data.cachedTables[db] = tables
else:
warnMsg = "unable to retrieve the table names "
warnMsg += "for database '%s'" % unsafeSQLIdentificatorNaming(db)
logger.warn(warnMsg)
if isNoneValue(kb.data.cachedTables):
kb.data.cachedTables.clear()
if not kb.data.cachedTables:
errMsg = "unable to retrieve the table names for any database"
if bruteForce is None:
logger.error(errMsg)
return self.getTables(bruteForce=True)
elif not conf.search:
raise SqlmapNoneDataException(errMsg)
2012-07-20 22:17:35 +04:00
else:
for db, tables in kb.data.cachedTables.items():
kb.data.cachedTables[db] = sorted(tables) if tables else tables
2012-12-18 12:55:33 +04:00
if kb.data.cachedTables:
for db in kb.data.cachedTables.keys():
kb.data.cachedTables[db] = list(set(kb.data.cachedTables[db]))
2012-07-20 22:17:35 +04:00
return kb.data.cachedTables
def getColumns(self, onlyColNames=False, colTuple=None, bruteForce=None, dumpMode=False):
2012-07-20 22:17:35 +04:00
self.forceDbmsEnum()
if conf.db is None or conf.db == CURRENT_DB:
if conf.db is None:
2012-10-04 20:28:36 +04:00
warnMsg = "missing database parameter. sqlmap is going "
2012-07-20 22:17:35 +04:00
warnMsg += "to use the current database to enumerate "
warnMsg += "table(s) columns"
logger.warn(warnMsg)
conf.db = self.getCurrentDb()
if not conf.db:
errMsg = "unable to retrieve the current "
errMsg += "database name"
raise SqlmapNoneDataException(errMsg)
2012-07-20 22:17:35 +04:00
elif conf.db is not None:
2018-10-16 14:26:55 +03:00
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.HSQLDB, DBMS.H2):
2012-07-20 22:17:35 +04:00
conf.db = conf.db.upper()
if ',' in conf.db:
2012-07-20 22:17:35 +04:00
errMsg = "only one database name is allowed when enumerating "
errMsg += "the tables' columns"
raise SqlmapMissingMandatoryOptionException(errMsg)
2012-07-20 22:17:35 +04:00
conf.db = safeSQLIdentificatorNaming(conf.db)
if conf.col:
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
conf.col = conf.col.upper()
2014-01-13 13:05:49 +04:00
colList = conf.col.split(',')
2012-07-20 22:17:35 +04:00
else:
colList = []
2018-02-13 17:53:50 +03:00
if conf.exclude:
colList = [_ for _ in colList if _ not in conf.exclude.split(',')]
2014-01-13 13:05:49 +04:00
2012-07-20 22:17:35 +04:00
for col in colList:
colList[colList.index(col)] = safeSQLIdentificatorNaming(col)
colList = filter(None, colList)
if conf.tbl:
2018-10-16 14:26:55 +03:00
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.HSQLDB, DBMS.H2):
2012-07-20 22:17:35 +04:00
conf.tbl = conf.tbl.upper()
2017-04-18 16:56:24 +03:00
tblList = conf.tbl.split(',')
2012-07-20 22:17:35 +04:00
else:
self.getTables()
if len(kb.data.cachedTables) > 0:
if conf.db in kb.data.cachedTables:
tblList = kb.data.cachedTables[conf.db]
else:
tblList = kb.data.cachedTables.values()
if isinstance(tblList[0], (set, tuple, list)):
tblList = tblList[0]
tblList = list(tblList)
elif not conf.search:
2012-07-20 22:17:35 +04:00
errMsg = "unable to retrieve the tables "
errMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
raise SqlmapNoneDataException(errMsg)
else:
return kb.data.cachedColumns
2012-07-20 22:17:35 +04:00
tblList = filter(None, (safeSQLIdentificatorNaming(_, True) for _ in tblList))
2012-07-20 22:17:35 +04:00
if bruteForce is None:
if Backend.isDbms(DBMS.MYSQL) and not kb.data.has_information_schema:
errMsg = "information_schema not available, "
errMsg += "back-end DBMS is MySQL < 5.0"
logger.error(errMsg)
bruteForce = True
elif Backend.isDbms(DBMS.ACCESS):
errMsg = "cannot retrieve column names, "
2016-05-31 13:23:59 +03:00
errMsg += "back-end DBMS is %s" % DBMS.ACCESS
2012-07-20 22:17:35 +04:00
logger.error(errMsg)
bruteForce = True
2012-12-18 19:31:30 +04:00
if bruteForce:
2012-07-20 22:17:35 +04:00
resumeAvailable = False
for tbl in tblList:
for db, table, colName, colType in kb.brute.columns:
if db == conf.db and table == tbl:
resumeAvailable = True
break
2014-02-05 18:11:39 +04:00
if resumeAvailable and not conf.freshQueries or colList:
2012-07-20 22:17:35 +04:00
columns = {}
for column in colList:
columns[column] = None
for tbl in tblList:
for db, table, colName, colType in kb.brute.columns:
if db == conf.db and table == tbl:
columns[colName] = colType
if conf.db in kb.data.cachedColumns:
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = columns
else:
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = {safeSQLIdentificatorNaming(tbl, True): columns}
return kb.data.cachedColumns
message = "do you want to use common column existence check? %s" % ("[Y/n/q]" if Backend.getIdentifiedDbms() in (DBMS.ACCESS,) else "[y/N/q]")
2017-04-19 15:46:27 +03:00
choice = readInput(message, default='Y' if 'Y' in message else 'N').upper()
2012-07-20 22:17:35 +04:00
2017-04-18 16:48:05 +03:00
if choice == 'N':
2012-07-20 22:17:35 +04:00
return
2017-04-18 16:48:05 +03:00
elif choice == 'Q':
raise SqlmapUserQuitException
2012-07-20 22:17:35 +04:00
else:
return columnExists(paths.COMMON_COLUMNS)
rootQuery = queries[Backend.getIdentifiedDbms()].columns
condition = rootQuery.blind.condition if 'condition' in rootQuery.blind else None
2012-12-05 13:45:17 +04:00
if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct:
2012-07-20 22:17:35 +04:00
for tbl in tblList:
if conf.db is not None and len(kb.data.cachedColumns) > 0 \
and conf.db in kb.data.cachedColumns and tbl in \
kb.data.cachedColumns[conf.db]:
infoMsg = "fetched tables' columns on "
infoMsg += "database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
logger.info(infoMsg)
return {conf.db: kb.data.cachedColumns[conf.db]}
infoMsg = "fetching columns "
2012-12-18 19:31:30 +04:00
condQuery = ""
2012-07-20 22:17:35 +04:00
if len(colList) > 0:
2012-12-18 19:31:30 +04:00
if colTuple:
_, colCondParam = colTuple
2015-09-22 13:03:47 +03:00
infoMsg += "LIKE '%s' " % ", ".join(unsafeSQLIdentificatorNaming(col) for col in sorted(colList))
2012-07-20 22:17:35 +04:00
else:
2012-12-18 19:31:30 +04:00
colCondParam = "='%s'"
2012-07-20 22:17:35 +04:00
infoMsg += "'%s' " % ", ".join(unsafeSQLIdentificatorNaming(col) for col in sorted(colList))
2012-12-18 19:31:30 +04:00
condQueryStr = "%%s%s" % colCondParam
condQuery = " AND (%s)" % " OR ".join(condQueryStr % (condition, unsafeSQLIdentificatorNaming(col)) for col in sorted(colList))
2012-07-20 22:17:35 +04:00
2018-10-16 13:23:07 +03:00
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2):
2012-07-20 22:17:35 +04:00
query = rootQuery.inband.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db))
query += condQuery
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
2013-01-19 00:44:56 +04:00
query = rootQuery.inband.query % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper()))
2012-07-20 22:17:35 +04:00
query += condQuery
elif Backend.isDbms(DBMS.MSSQL):
query = rootQuery.inband.query % (conf.db, conf.db, conf.db, conf.db,
conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl).split(".")[-1])
query += condQuery.replace("[DB]", conf.db)
2013-01-19 02:10:10 +04:00
elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.FIREBIRD):
2017-09-05 00:00:16 +03:00
query = rootQuery.inband.query % unsafeSQLIdentificatorNaming(tbl)
2013-06-24 18:03:08 +04:00
if dumpMode and colList:
values = [(_,) for _ in colList]
else:
infoMsg += "for table '%s' " % unsafeSQLIdentificatorNaming(tbl)
infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
logger.info(infoMsg)
values = None
if Backend.isDbms(DBMS.MSSQL) and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION):
expression = query
kb.dumpColumns = []
kb.rowXmlMode = True
for column in extractRegexResult(r"SELECT (?P<result>.+?) FROM", query).split(','):
kb.dumpColumns.append(randomStr().lower())
expression = expression.replace(column, "%s AS %s" % (column, kb.dumpColumns[-1]), 1)
values = unionUse(expression)
kb.rowXmlMode = False
kb.dumpColumns = None
if values is None:
values = inject.getValue(query, blind=False, time=False)
2018-08-22 18:58:00 +03:00
if values and isinstance(values[0], basestring):
values = [values]
2012-07-20 22:17:35 +04:00
2013-01-11 14:17:41 +04:00
if Backend.isDbms(DBMS.MSSQL) and isNoneValue(values):
index, values = 1, []
2013-01-19 00:40:38 +04:00
2013-01-11 14:17:41 +04:00
while True:
2017-09-05 00:00:16 +03:00
query = rootQuery.inband.query2 % (conf.db, unsafeSQLIdentificatorNaming(tbl), index)
2013-01-11 14:17:41 +04:00
value = unArrayizeValue(inject.getValue(query, blind=False, time=False))
2013-01-19 00:40:38 +04:00
2013-01-11 14:17:41 +04:00
if isNoneValue(value) or value == " ":
break
else:
values.append((value,))
index += 1
2012-07-20 22:17:35 +04:00
if Backend.isDbms(DBMS.SQLITE):
parseSqliteTableSchema(unArrayizeValue(values))
elif not isNoneValue(values):
2012-07-20 22:17:35 +04:00
table = {}
columns = {}
for columnData in values:
2012-07-20 22:17:35 +04:00
if not isNoneValue(columnData):
name = safeSQLIdentificatorNaming(columnData[0])
if name:
2013-07-29 20:25:27 +04:00
if conf.getComments:
_ = queries[Backend.getIdentifiedDbms()].column_comment
if hasattr(_, "query"):
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
query = _.query % (unsafeSQLIdentificatorNaming(conf.db.upper()), unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(name.upper()))
else:
query = _.query % (unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(name))
2016-04-19 14:13:37 +03:00
2013-07-29 20:25:27 +04:00
comment = unArrayizeValue(inject.getValue(query, blind=False, time=False))
2016-04-19 14:13:37 +03:00
if not isNoneValue(comment):
infoMsg = "retrieved comment '%s' for column '%s'" % (comment, name)
logger.info(infoMsg)
2013-07-29 20:25:27 +04:00
else:
warnMsg = "on %s it is not " % Backend.getIdentifiedDbms()
warnMsg += "possible to get column comments"
singleTimeWarnMessage(warnMsg)
2012-07-20 22:17:35 +04:00
if len(columnData) == 1:
2013-01-11 14:17:41 +04:00
columns[name] = None
2012-07-20 22:17:35 +04:00
else:
2016-09-23 19:03:31 +03:00
key = int(columnData[1]) if isinstance(columnData[1], basestring) and columnData[1].isdigit() else columnData[1]
if Backend.isDbms(DBMS.FIREBIRD):
2016-09-23 19:03:31 +03:00
columnData[1] = FIREBIRD_TYPES.get(key, columnData[1])
elif Backend.isDbms(DBMS.INFORMIX):
notNull = False
if isinstance(key, int) and key > 255:
key -= 256
notNull = True
columnData[1] = INFORMIX_TYPES.get(key, columnData[1])
if notNull:
columnData[1] = "%s NOT NULL" % columnData[1]
2012-07-20 22:17:35 +04:00
columns[name] = columnData[1]
if conf.db in kb.data.cachedColumns:
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = columns
else:
table[safeSQLIdentificatorNaming(tbl, True)] = columns
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = table
elif isInferenceAvailable() and not conf.direct:
for tbl in tblList:
if conf.db is not None and len(kb.data.cachedColumns) > 0 \
and conf.db in kb.data.cachedColumns and tbl in \
kb.data.cachedColumns[conf.db]:
infoMsg = "fetched tables' columns on "
2013-02-15 19:48:58 +04:00
infoMsg += "database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
2012-07-20 22:17:35 +04:00
logger.info(infoMsg)
return {conf.db: kb.data.cachedColumns[conf.db]}
infoMsg = "fetching columns "
2012-12-18 19:31:30 +04:00
condQuery = ""
2012-07-20 22:17:35 +04:00
if len(colList) > 0:
2012-12-18 19:31:30 +04:00
if colTuple:
_, colCondParam = colTuple
2015-09-22 13:03:47 +03:00
infoMsg += "LIKE '%s' " % ", ".join(unsafeSQLIdentificatorNaming(col) for col in sorted(colList))
2012-07-20 22:17:35 +04:00
else:
2012-12-18 19:31:30 +04:00
colCondParam = "='%s'"
2012-07-20 22:17:35 +04:00
infoMsg += "'%s' " % ", ".join(unsafeSQLIdentificatorNaming(col) for col in sorted(colList))
2012-12-18 19:31:30 +04:00
condQueryStr = "%%s%s" % colCondParam
condQuery = " AND (%s)" % " OR ".join(condQueryStr % (condition, unsafeSQLIdentificatorNaming(col)) for col in sorted(colList))
2012-07-20 22:17:35 +04:00
2018-10-16 13:23:07 +03:00
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2):
2012-07-20 22:17:35 +04:00
query = rootQuery.blind.count % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db))
query += condQuery
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
2013-01-19 00:44:56 +04:00
query = rootQuery.blind.count % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper()))
2012-07-20 22:17:35 +04:00
query += condQuery
elif Backend.isDbms(DBMS.MSSQL):
query = rootQuery.blind.count % (conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl).split(".")[-1])
2012-07-20 22:17:35 +04:00
query += condQuery.replace("[DB]", conf.db)
elif Backend.isDbms(DBMS.FIREBIRD):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.count % unsafeSQLIdentificatorNaming(tbl)
2012-07-20 22:17:35 +04:00
query += condQuery
elif Backend.isDbms(DBMS.INFORMIX):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.count % (conf.db, conf.db, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl))
query += condQuery
2012-07-20 22:17:35 +04:00
elif Backend.isDbms(DBMS.SQLITE):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query % unsafeSQLIdentificatorNaming(tbl)
value = unArrayizeValue(inject.getValue(query, union=False, error=False))
2012-07-20 22:17:35 +04:00
parseSqliteTableSchema(value)
return kb.data.cachedColumns
table = {}
columns = {}
if dumpMode and colList:
count = 0
for value in colList:
columns[safeSQLIdentificatorNaming(value)] = None
else:
infoMsg += "for table '%s' " % unsafeSQLIdentificatorNaming(tbl)
infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
logger.info(infoMsg)
count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
if not isNumPosStrValue(count):
if Backend.isDbms(DBMS.MSSQL):
count, index, values = 0, 1, []
while True:
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query3 % (conf.db, unsafeSQLIdentificatorNaming(tbl), index)
value = unArrayizeValue(inject.getValue(query, union=False, error=False))
if isNoneValue(value) or value == " ":
break
else:
columns[safeSQLIdentificatorNaming(value)] = None
index += 1
2013-01-11 14:17:41 +04:00
if not columns:
errMsg = "unable to retrieve the %scolumns " % ("number of " if not Backend.isDbms(DBMS.MSSQL) else "")
errMsg += "for table '%s' " % unsafeSQLIdentificatorNaming(tbl)
errMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
logger.error(errMsg)
continue
2013-01-11 14:17:41 +04:00
2012-09-07 19:06:38 +04:00
for index in getLimitRange(count):
2018-10-16 15:47:09 +03:00
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB):
2012-07-20 22:17:35 +04:00
query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db))
query += condQuery
field = None
2018-10-16 15:47:09 +03:00
elif Backend.isDbms(DBMS.H2):
query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(conf.db))
query = query.replace(" ORDER BY ", "%s ORDER BY " % condQuery)
field = None
2012-07-20 22:17:35 +04:00
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
2013-01-19 00:44:56 +04:00
query = rootQuery.blind.query % (unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(conf.db.upper()))
2012-07-20 22:17:35 +04:00
query += condQuery
field = None
elif Backend.isDbms(DBMS.MSSQL):
2012-09-07 19:06:38 +04:00
query = rootQuery.blind.query.replace("'%s'", "'%s'" % unsafeSQLIdentificatorNaming(tbl).split(".")[-1]).replace("%s", conf.db).replace("%d", str(index))
2012-07-20 22:17:35 +04:00
query += condQuery.replace("[DB]", conf.db)
field = condition.replace("[DB]", conf.db)
elif Backend.isDbms(DBMS.FIREBIRD):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query % unsafeSQLIdentificatorNaming(tbl)
2012-07-20 22:17:35 +04:00
query += condQuery
field = None
elif Backend.isDbms(DBMS.INFORMIX):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query % (index, conf.db, conf.db, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl))
query += condQuery
field = condition
2012-07-20 22:17:35 +04:00
2012-09-07 19:06:38 +04:00
query = agent.limitQuery(index, query, field, field)
column = unArrayizeValue(inject.getValue(query, union=False, error=False))
2012-07-20 22:17:35 +04:00
if not isNoneValue(column):
2013-07-29 20:25:27 +04:00
if conf.getComments:
_ = queries[Backend.getIdentifiedDbms()].column_comment
if hasattr(_, "query"):
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
query = _.query % (unsafeSQLIdentificatorNaming(conf.db.upper()), unsafeSQLIdentificatorNaming(tbl.upper()), unsafeSQLIdentificatorNaming(column.upper()))
else:
query = _.query % (unsafeSQLIdentificatorNaming(conf.db), unsafeSQLIdentificatorNaming(tbl), unsafeSQLIdentificatorNaming(column))
2016-04-19 14:13:37 +03:00
2013-07-29 20:25:27 +04:00
comment = unArrayizeValue(inject.getValue(query, union=False, error=False))
2016-04-19 14:13:37 +03:00
if not isNoneValue(comment):
infoMsg = "retrieved comment '%s' for column '%s'" % (comment, column)
logger.info(infoMsg)
2013-07-29 20:25:27 +04:00
else:
warnMsg = "on %s it is not " % Backend.getIdentifiedDbms()
warnMsg += "possible to get column comments"
singleTimeWarnMessage(warnMsg)
2012-07-20 22:17:35 +04:00
if not onlyColNames:
2018-10-16 13:23:07 +03:00
if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB, DBMS.H2):
2012-07-20 22:17:35 +04:00
query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column, unsafeSQLIdentificatorNaming(conf.db))
elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
2013-01-19 00:44:56 +04:00
query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl.upper()), column, unsafeSQLIdentificatorNaming(conf.db.upper()))
2012-07-20 22:17:35 +04:00
elif Backend.isDbms(DBMS.MSSQL):
query = rootQuery.blind.query2 % (conf.db, conf.db, conf.db, conf.db, column, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl).split(".")[-1])
2012-07-20 22:17:35 +04:00
elif Backend.isDbms(DBMS.FIREBIRD):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query2 % (unsafeSQLIdentificatorNaming(tbl), column)
2016-09-23 19:03:31 +03:00
elif Backend.isDbms(DBMS.INFORMIX):
2017-09-05 00:00:16 +03:00
query = rootQuery.blind.query2 % (conf.db, conf.db, conf.db, conf.db, conf.db, unsafeSQLIdentificatorNaming(tbl), column)
2012-07-20 22:17:35 +04:00
colType = unArrayizeValue(inject.getValue(query, union=False, error=False))
2012-07-20 22:17:35 +04:00
2016-09-23 19:03:31 +03:00
key = int(colType) if isinstance(colType, basestring) and colType.isdigit() else colType
2012-07-20 22:17:35 +04:00
if Backend.isDbms(DBMS.FIREBIRD):
2016-09-23 19:03:31 +03:00
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
2012-07-20 22:17:35 +04:00
column = safeSQLIdentificatorNaming(column)
columns[column] = colType
else:
column = safeSQLIdentificatorNaming(column)
columns[column] = None
if columns:
if conf.db in kb.data.cachedColumns:
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] = columns
else:
table[safeSQLIdentificatorNaming(tbl, True)] = columns
kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] = table
if not kb.data.cachedColumns:
2012-12-18 20:42:03 +04:00
warnMsg = "unable to retrieve column names for "
2013-02-15 19:48:58 +04:00
warnMsg += ("table '%s' " % unsafeSQLIdentificatorNaming(unArrayizeValue(tblList))) if len(tblList) == 1 else "any table "
2012-12-18 20:42:03 +04:00
warnMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db)
logger.warn(warnMsg)
2012-07-20 22:17:35 +04:00
if bruteForce is None:
return self.getColumns(onlyColNames=onlyColNames, colTuple=colTuple, bruteForce=True)
return kb.data.cachedColumns
@stackedmethod
2012-07-20 22:17:35 +04:00
def getSchema(self):
infoMsg = "enumerating database management system schema"
logger.info(infoMsg)
2015-07-18 18:01:34 +03:00
try:
pushValue(conf.db)
pushValue(conf.tbl)
pushValue(conf.col)
2012-07-20 22:17:35 +04:00
2015-07-18 18:01:34 +03:00
kb.data.cachedTables = {}
kb.data.cachedColumns = {}
2012-07-20 22:17:35 +04:00
2015-07-18 18:01:34 +03:00
self.getTables()
2012-07-20 22:17:35 +04:00
2015-07-18 18:01:34 +03:00
infoMsg = "fetched tables: "
infoMsg += ", ".join(["%s" % ", ".join("%s%s%s" % (unsafeSQLIdentificatorNaming(db), ".." if Backend.isDbms(DBMS.MSSQL) or Backend.isDbms(DBMS.SYBASE) else '.', unsafeSQLIdentificatorNaming(_)) for _ in tbl) for db, tbl in kb.data.cachedTables.items()])
2015-07-18 18:01:34 +03:00
logger.info(infoMsg)
2012-07-20 22:17:35 +04:00
2015-07-18 18:01:34 +03:00
for db, tables in kb.data.cachedTables.items():
for tbl in tables:
conf.db = db
conf.tbl = tbl
self.getColumns()
finally:
conf.col = popValue()
conf.tbl = popValue()
conf.db = popValue()
2012-07-20 22:17:35 +04:00
return kb.data.cachedColumns
def _tableGetCount(self, db, table):
2014-12-30 12:04:41 +03:00
if not db or not table:
return None
2013-01-18 02:13:59 +04:00
if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2):
db = db.upper()
table = table.upper()
2012-07-20 22:17:35 +04:00
if Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD):
query = "SELECT %s FROM %s" % (queries[Backend.getIdentifiedDbms()].count.query % '*', safeSQLIdentificatorNaming(table, True))
else:
query = "SELECT %s FROM %s.%s" % (queries[Backend.getIdentifiedDbms()].count.query % '*', safeSQLIdentificatorNaming(db), safeSQLIdentificatorNaming(table, True))
2012-07-20 22:17:35 +04:00
count = inject.getValue(query, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS)
if isNumPosStrValue(count):
if safeSQLIdentificatorNaming(db) not in kb.data.cachedCounts:
kb.data.cachedCounts[safeSQLIdentificatorNaming(db)] = {}
if int(count) in kb.data.cachedCounts[safeSQLIdentificatorNaming(db)]:
kb.data.cachedCounts[safeSQLIdentificatorNaming(db)][int(count)].append(safeSQLIdentificatorNaming(table, True))
else:
kb.data.cachedCounts[safeSQLIdentificatorNaming(db)][int(count)] = [safeSQLIdentificatorNaming(table, True)]
def getCount(self):
if not conf.tbl:
warnMsg = "missing table parameter, sqlmap will retrieve "
warnMsg += "the number of entries for all database "
warnMsg += "management system databases' tables"
logger.warn(warnMsg)
elif "." in conf.tbl:
if not conf.db:
2015-09-09 15:46:06 +03:00
conf.db, conf.tbl = conf.tbl.split('.', 1)
2012-07-20 22:17:35 +04:00
if conf.tbl is not None and conf.db is None and Backend.getIdentifiedDbms() not in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD):
2012-10-04 20:28:36 +04:00
warnMsg = "missing database parameter. sqlmap is going to "
2012-07-20 22:17:35 +04:00
warnMsg += "use the current database to retrieve the "
warnMsg += "number of entries for table '%s'" % unsafeSQLIdentificatorNaming(conf.tbl)
logger.warn(warnMsg)
conf.db = self.getCurrentDb()
self.forceDbmsEnum()
if conf.tbl:
2017-04-18 16:56:24 +03:00
for table in conf.tbl.split(','):
self._tableGetCount(conf.db, table)
2012-07-20 22:17:35 +04:00
else:
self.getTables()
for db, tables in kb.data.cachedTables.items():
for table in tables:
self._tableGetCount(db, table)
2012-07-20 22:17:35 +04:00
return kb.data.cachedCounts