diff --git a/lib/core/common.py b/lib/core/common.py index a4be08272..ab7cbe4e9 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -1967,19 +1967,21 @@ def getLimitRange(count, plusOne=False): if kb.dumpTable: if conf.limitStart and conf.limitStop and conf.limitStart > conf.limitStop: - limitStop = conf.limitStart + limitStop = min(conf.limitStart, count) # a '--start' beyond the table must not request out-of-range offsets (phantom rows) limitStart = conf.limitStop reverse = True else: if isinstance(conf.limitStop, int) and conf.limitStop > 0 and conf.limitStop < limitStop: limitStop = conf.limitStop - if isinstance(conf.limitStart, int) and conf.limitStart > 0 and conf.limitStart <= limitStop: + # NOTE: no '<= limitStop' gate - a '--start' past the row count must yield an EMPTY range + # (correctly skipping past every row), not silently fall back to dumping the whole table + if isinstance(conf.limitStart, int) and conf.limitStart > 0: limitStart = conf.limitStart retVal = xrange(limitStart, limitStop + 1) if plusOne else xrange(limitStart - 1, limitStop) - if reverse: + if reverse and len(retVal): # len() guard: a clamped out-of-range '--start' can leave the range empty retVal = xrange(retVal[-1], retVal[0] - 1, -1) return retVal diff --git a/lib/core/convert.py b/lib/core/convert.py index bb57e4c18..39d7028dc 100644 --- a/lib/core/convert.py +++ b/lib/core/convert.py @@ -577,7 +577,7 @@ def getUnicode(value, encoding=None, noneToNull=False): try: return six.text_type(value, encoding or (kb.get("pageEncoding") if kb.get("originalPage") else None) or UNICODE_ENCODING) - except UnicodeDecodeError: + except (UnicodeDecodeError, LookupError): # LookupError: an unknown/invalid encoding name must fall back, not crash return six.text_type(value, UNICODE_ENCODING, errors="reversible") elif isListLike(value): value = list(getUnicode(_, encoding, noneToNull) for _ in value) diff --git a/lib/core/option.py b/lib/core/option.py index babcf675a..5e8b3b54e 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -520,7 +520,8 @@ def _setOpenApiTargets(): checkFile(conf.openApiFile) infoMsg = "parsing OpenAPI/Swagger specification from '%s'" % conf.openApiFile logger.info(infoMsg) - content = openFile(conf.openApiFile).read() + with openFile(conf.openApiFile) as f: + content = f.read() tags = [_.strip() for _ in re.split(PARAMETER_SPLITTING_REGEX, conf.openApiTags) if _.strip()] if conf.openApiTags else None if tags: @@ -835,7 +836,8 @@ def _listTamperingFunctions(): logger.info(infoMsg) for script in sorted(glob.glob(os.path.join(paths.SQLMAP_TAMPER_PATH, "*.py"))): - content = openFile(script, 'r').read() + with openFile(script, 'r') as f: + content = f.read() match = re.search(r'(?s)__priority__.+"""(.+)"""', content) if match: comment = match.group(1).strip() diff --git a/lib/core/settings.py b/lib/core/settings.py index 4b5714495..e6b6c7c16 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -20,7 +20,7 @@ from lib.core.enums import OS from thirdparty import six # sqlmap version (...) -VERSION = "1.10.7.242" +VERSION = "1.10.7.243" 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) @@ -533,8 +533,11 @@ ERROR_PARSING_REGEXES = ( r'"(?:errmsg|errorMessage|reason|msg)"\s*:\s*"(?P[^"]+)"' # generic JSON error-message field (NoSQL document/REST back-ends) ) -# Regular expression used for parsing charset info from meta html headers -META_CHARSET_REGEX = r"""(?si)]*>.*]+charset\s*=\s*["']?(?P[^"'> ]+).*""" +# Regular expression used for parsing charset info from meta html headers (Note: the tempered token +# '(?:(?!).)*?' keeps the meta strictly INSIDE - as the old trailing '.*' did - +# while the bounded meta-attr scan '{0,300}?' keeps it LINEAR; the old greedy form went quadratic and +# hung for many minutes on an attacker-controlled body full of ''/'') +META_CHARSET_REGEX = r"""(?si)]*>(?:(?!).)*?]{0,300}?charset\s*=\s*["']?(?P[^"'> ]+)""" # Regular expression used for parsing refresh info from meta html headers META_REFRESH_REGEX = r'(?i)]+content="?[^">]+;\s*(url=)?["\']?(?P[^\'">]+)' diff --git a/lib/request/comparison.py b/lib/request/comparison.py index cb49bc179..c9a4dcc1f 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -67,7 +67,7 @@ def comparison(page, headers, code=None, getRatioValue=False, pageLength=None): return _ def _adjust(condition, getRatioValue): - if not any((conf.string, conf.notString, conf.regexp, conf.code)): + if not any((conf.string, conf.notString, conf.regexp, conf.code, conf.lengths)): # Negative logic approach is used in raw page comparison scheme as that what is "different" than original # PAYLOAD.WHERE.NEGATIVE response is considered as True; in switch based approach negative logic is not # applied as that what is by user considered as True is that what is returned by the comparison mechanism diff --git a/lib/request/connect.py b/lib/request/connect.py index 01a4ae86f..6f990c89a 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -406,7 +406,8 @@ class Connect(object): errMsg = "problem occurred while loading cookies from file '%s'" % conf.liveCookies raise SqlmapValueException(errMsg) - cookie = openFile(conf.liveCookies).read().strip() + with openFile(conf.liveCookies) as f: + cookie = f.read().strip() cookie = re.sub(r"(?i)\ACookie:\s*", "", cookie) if multipart: @@ -545,7 +546,8 @@ class Connect(object): headers = forgeHeaders(auxHeaders, headers) if kb.headersFile: - content = openFile(kb.headersFile, 'r').read() + with openFile(kb.headersFile, 'r') as f: + content = f.read() for line in content.split("\n"): line = getText(line.strip()) if ':' in line: diff --git a/lib/utils/api.py b/lib/utils/api.py index 1a0794ec1..cf29f396d 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -875,7 +875,8 @@ def download(taskid, target, filename): if os.path.isfile(path): logger.debug("(%s) Retrieved content of file %s" % (taskid, target)) - content = openFile(path, "rb").read() + with openFile(path, "rb") as f: + content = f.read() return jsonize({"success": True, "file": encodeBase64(content, binary=False)}) else: logger.warning("[%s] File does not exist %s" % (taskid, target)) diff --git a/lib/utils/hash.py b/lib/utils/hash.py index 211738b99..c73e90d0e 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -1117,7 +1117,7 @@ def _bruteProcessVariantA(attack_info, hash_regex, suffix, retVal, proc_id, proc word = word + suffix try: - current = __functions__[hash_regex](password=word, uppercase=False) + current = __functions__[hash_regex](password=getBytes(word, unsafe=False), uppercase=False) if current in hashes: for item in attack_info[:]: @@ -1195,7 +1195,7 @@ def _bruteProcessVariantB(user, hash_, kwargs, hash_regex, suffix, retVal, found word = word + suffix try: - current = __functions__[hash_regex](password=word, uppercase=False, **kwargs) + current = __functions__[hash_regex](password=getBytes(word, unsafe=False), uppercase=False, **kwargs) if hash_ == current: if hash_regex == HASH.ORACLE_OLD: # only for cosmetic purposes @@ -1285,7 +1285,7 @@ def _bruteProcessVariantSalted(attack_info, hash_regex, suffix, retVal, proc_id, ((user, hash_), kwargs) = item try: - current = __functions__[hash_regex](password=word, uppercase=False, **kwargs) + current = __functions__[hash_regex](password=getBytes(word, unsafe=False), uppercase=False, **kwargs) if hash_ == current: retVal.put((user, hash_, word)) diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 0c8a2f289..7d2f61335 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -333,7 +333,9 @@ class Entries(object): kb.data.dumpedTable[column] = {"length": len(column), "values": BigArray()} for entry in entries: - if entry is None or len(entry) == 0: + # skip a missing/empty ROW container, but NOT an empty-string CELL value + # (single-column dumps yield bare strings; len("")==0 must not drop the row) + if entry is None or (isListLike(entry) and len(entry) == 0): continue if isinstance(entry, six.string_types): diff --git a/plugins/generic/search.py b/plugins/generic/search.py index 5c444e3db..61c9ff6cd 100644 --- a/plugins/generic/search.py +++ b/plugins/generic/search.py @@ -135,8 +135,9 @@ class Search(object): query = agent.limitQuery(index, query, dbCond) value = unArrayizeValue(inject.getValue(query, union=False, error=False)) - value = safeSQLIdentificatorNaming(value) - foundDbs.append(value) + if not isNoneValue(value): # guard (mirrors searchTable) so a failed retrieval can't push a None/garbage name + value = safeSQLIdentificatorNaming(value) + foundDbs.append(value) conf.dumper.lister("found databases", foundDbs) diff --git a/plugins/generic/users.py b/plugins/generic/users.py index 7bd070510..9ce87db65 100644 --- a/plugins/generic/users.py +++ b/plugins/generic/users.py @@ -627,17 +627,18 @@ class Users(object): elif Backend.isDbms(DBMS.DB2): privs = privilege.split(',') privilege = privs[0] - privs = privs[1] - privs = list(privs.strip()) - i = 1 + if len(privs) > 1: # guard a comma-less privilege value (mirrors the inband path) + privs = privs[1] + privs = list(privs.strip()) + i = 1 - for priv in privs: - if priv.upper() in ('Y', 'G'): - for position, db2Priv in DB2_PRIVS.items(): - if position == i: - privilege += ", " + db2Priv + for priv in privs: + if priv.upper() in ('Y', 'G'): + for position, db2Priv in DB2_PRIVS.items(): + if position == i: + privilege += ", " + db2Priv - i += 1 + i += 1 privileges.add(privilege)