From a33b0454cdb6db90dc6e848be23b063ab5f5ce22 Mon Sep 17 00:00:00 2001 From: Miroslav Stampar Date: Wed, 26 Aug 2015 15:26:16 +0200 Subject: [PATCH] Implementation for an Issue #1360 --- lib/core/enums.py | 1 + lib/core/option.py | 1 + lib/core/settings.py | 8 +++--- lib/core/target.py | 10 +++++-- lib/techniques/error/use.py | 52 +++++++++++++++++++++++++------------ 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/lib/core/enums.py b/lib/core/enums.py index cb1b7b36f..1ba4b8185 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -197,6 +197,7 @@ class HASHDB_KEYS: KB_CHARS = "KB_CHARS" KB_DYNAMIC_MARKINGS = "KB_DYNAMIC_MARKINGS" KB_INJECTIONS = "KB_INJECTIONS" + KB_ERROR_CHUNK_LENGTH = "KB_ERROR_CHUNK_LENGTH" KB_XP_CMDSHELL_AVAILABLE = "KB_XP_CMDSHELL_AVAILABLE" OS = "OS" diff --git a/lib/core/option.py b/lib/core/option.py index ad45bb3f7..b4f9faea1 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1792,6 +1792,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.endDetection = False kb.explicitSettings = set() kb.extendTests = None + kb.errorChunkLength = None kb.errorIsNone = True kb.fileReadMode = False kb.followSitemapRecursion = None diff --git a/lib/core/settings.py b/lib/core/settings.py index 22b0cf3bc..2457770d1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -323,11 +323,11 @@ CUSTOM_INJECTION_MARK_CHAR = '*' # Other way to declare injection position INJECT_HERE_MARK = '%INJECT HERE%' -# Maximum length used for retrieving data over MySQL error based payload due to "known" problems with longer result strings -MYSQL_ERROR_CHUNK_LENGTH = 50 +# Minimum chunk length used for retrieving data over error based payloads +MIN_ERROR_CHUNK_LENGTH = 8 -# Maximum length used for retrieving data over MSSQL error based payload due to trimming problems with longer result strings -MSSQL_ERROR_CHUNK_LENGTH = 100 +# Maximum chunk length used for retrieving data over error based payloads +MAX_ERROR_CHUNK_LENGTH = 1024 # Do not escape the injected statement if it contains any of the following SQL keywords EXCLUDE_UNESCAPE = ("WAITFOR DELAY ", " INTO DUMPFILE ", " INTO OUTFILE ", "CREATE ", "BULK ", "EXEC ", "RECONFIGURE ", "DECLARE ", "'%s'" % CHAR_INFERENCE_MARK) diff --git a/lib/core/target.py b/lib/core/target.py index 1c35b3515..50158817a 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -403,12 +403,18 @@ def _resumeHashDBValues(): """ kb.absFilePaths = hashDBRetrieve(HASHDB_KEYS.KB_ABS_FILE_PATHS, True) or kb.absFilePaths - kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars - kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings kb.brute.tables = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_TABLES, True) or kb.brute.tables kb.brute.columns = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_COLUMNS, True) or kb.brute.columns + kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars + kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings kb.xpCmdshellAvailable = hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) or kb.xpCmdshellAvailable + kb.errorChunkLength = hashDBRetrieve(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH) + if kb.errorChunkLength and kb.errorChunkLength.isdigit(): + kb.errorChunkLength = int(kb.errorChunkLength) + else: + kb.errorChunkLength = None + conf.tmpPath = conf.tmpPath or hashDBRetrieve(HASHDB_KEYS.CONF_TMP_PATH) for injection in hashDBRetrieve(HASHDB_KEYS.KB_INJECTIONS, True) or []: diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index a647d89f2..8a8009d34 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -35,10 +35,11 @@ from lib.core.data import logger from lib.core.data import queries from lib.core.dicts import FROM_DUMMY_TABLE from lib.core.enums import DBMS +from lib.core.enums import HASHDB_KEYS from lib.core.enums import HTTP_HEADER from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD -from lib.core.settings import MYSQL_ERROR_CHUNK_LENGTH -from lib.core.settings import MSSQL_ERROR_CHUNK_LENGTH +from lib.core.settings import MIN_ERROR_CHUNK_LENGTH +from lib.core.settings import MAX_ERROR_CHUNK_LENGTH from lib.core.settings import NULL from lib.core.settings import PARTIAL_VALUE_MARKER from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD @@ -50,7 +51,7 @@ from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.utils.progress import ProgressBar -def _oneShotErrorUse(expression, field=None): +def _oneShotErrorUse(expression, field=None, chunkTest=False): offset = 1 partialValue = None threadData = getCurrentThreadData() @@ -63,12 +64,28 @@ def _oneShotErrorUse(expression, field=None): threadData.resumed = retVal is not None and not partialValue - if Backend.isDbms(DBMS.MYSQL): - chunkLength = MYSQL_ERROR_CHUNK_LENGTH - elif Backend.isDbms(DBMS.MSSQL): - chunkLength = MSSQL_ERROR_CHUNK_LENGTH - else: - chunkLength = None + if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and kb.errorChunkLength is None and not chunkTest and not kb.testMode: + debugMsg = "searching for error chunk length..." + logger.debug(debugMsg) + + current = MAX_ERROR_CHUNK_LENGTH + while current >= MIN_ERROR_CHUNK_LENGTH: + testChar = str(current % 10) + testQuery = "SELECT %s('%s',%d)" % ("REPEAT" if Backend.isDbms(DBMS.MYSQL) else "REPLICATE", testChar, current) + result = unArrayizeValue(_oneShotErrorUse(testQuery, chunkTest=True)) + if result and testChar in result: + if result == testChar * current: + kb.errorChunkLength = current + break + else: + current = len(result) - len(kb.chars.stop) + else: + current = current / 2 + + if kb.errorChunkLength: + hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, kb.errorChunkLength) + else: + kb.errorChunkLength = 0 if retVal is None or partialValue: try: @@ -79,12 +96,12 @@ def _oneShotErrorUse(expression, field=None): if field: nulledCastedField = agent.nullAndCastField(field) - if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and not any(_ in field for _ in ("COUNT", "CASE")): # skip chunking of scalar expression (unneeded) + if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and not any(_ in field for _ in ("COUNT", "CASE")) and kb.errorChunkLength and not chunkTest: extendedField = re.search(r"[^ ,]*%s[^ ,]*" % re.escape(field), expression).group(0) if extendedField != field: # e.g. MIN(surname) nulledCastedField = extendedField.replace(field, nulledCastedField) field = extendedField - nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, chunkLength) + nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, kb.errorChunkLength) # Forge the error-based SQL injection request vector = kb.injection.data[kb.technique].vector @@ -125,10 +142,11 @@ def _oneShotErrorUse(expression, field=None): threadData.lastRequestUID else None, re.DOTALL | re.IGNORECASE) if trimmed: - warnMsg = "possible server trimmed output detected " - warnMsg += "(due to its length and/or content): " - warnMsg += safecharencode(trimmed) - logger.warn(warnMsg) + if not chunkTest: + warnMsg = "possible server trimmed output detected " + warnMsg += "(due to its length and/or content): " + warnMsg += safecharencode(trimmed) + logger.warn(warnMsg) if not kb.testMode: check = "(?P.*?)%s" % kb.chars.stop[:2] @@ -146,8 +164,8 @@ def _oneShotErrorUse(expression, field=None): else: retVal += output if output else '' - if output and len(output) >= chunkLength: - offset += chunkLength + if output and kb.errorChunkLength and len(output) >= kb.errorChunkLength and not chunkTest: + offset += kb.errorChunkLength else: break