diff --git a/lib/core/option.py b/lib/core/option.py index aba1d4a1c..7a23ff1b5 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -423,7 +423,10 @@ def _doSearch(): conf.googlePage += 1 def _setStdinPipeTargets(): - if conf.url: + # Note: an explicit target source takes precedence. Without this, any non-interactive run (CI, + # cron, subprocess) would reroute '-m/-l/-r/-g' targets through the STDIN container, losing both + # their count and their order + if any((conf.url, conf.direct, conf.logFile, conf.bulkFile, conf.requestFile, conf.googleDork, conf.openApiFile)): return if isinstance(conf.stdinPipe, _collections.Iterable): diff --git a/lib/core/settings.py b/lib/core/settings.py index 998a58b65..6266dad78 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.8.42" +VERSION = "1.10.8.43" 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) @@ -88,7 +88,7 @@ WAF_BYPASS_TAMPERS = ( ) # Maximum number of candidate tamper (chains) trialled during automatic WAF-bypass -WAF_BYPASS_MAX_TRIALS = 8 +WAF_BYPASS_MAX_TRIALS = len(WAF_BYPASS_TAMPERS) # Browser-like request headers applied alongside the random (non-scanner) User-Agent during # automatic WAF bypass: sqlmap's defaults ('Accept: */*', no 'Accept-Language') are themselves a diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 2f3e66365..2359f585f 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -1215,7 +1215,10 @@ def cmdLineParser(argv=None): if args.dummy: args.url = args.url or DUMMY_URL - if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ)): + # Note: an explicit target source rules out reading targets from the standard input. Without + # this, any non-interactive run (CI, cron, subprocess) would turn '-d/-u/-m/-l/-r/-g' into a + # 'multiple targets' run reading from a pipe, which also resets per-target options in between + if hasattr(sys.stdin, "fileno") and not any((os.isatty(sys.stdin.fileno()), args.api, args.ignoreStdin, "GITHUB_ACTIONS" in os.environ, args.direct, args.url, args.logFile, args.bulkFile, args.requestFile, args.googleDork, args.configFile, args.openApiFile)): args.stdinPipe = iter(sys.stdin.readline, None) else: args.stdinPipe = None diff --git a/plugins/dbms/mysql/fingerprint.py b/plugins/dbms/mysql/fingerprint.py index 18874b856..84eabbdbe 100644 --- a/plugins/dbms/mysql/fingerprint.py +++ b/plugins/dbms/mysql/fingerprint.py @@ -120,7 +120,7 @@ class Fingerprint(GenericFingerprint): fork = FORK.DORIS elif inject.checkBooleanExpression("@@VERSION_COMMENT LIKE '%StarRocks%'"): fork = FORK.STARROCKS - elif inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL"): # Note: MemSQL is the only one of the forks without SESSION_USER() + elif inject.checkBooleanExpression("GEOGRAPHY_AREA(NULL) IS NULL"): # Note: GEOGRAPHY_AREA() is MemSQL specific fork = FORK.MEMSQL elif inject.checkBooleanExpression("AURORA_VERSION() LIKE '%'"): # Reference: https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/ fork = FORK.AURORA diff --git a/tamper/blindbinary.py b/tamper/blindbinary.py index 7eb3999e4..9e4f6e8b9 100644 --- a/tamper/blindbinary.py +++ b/tamper/blindbinary.py @@ -7,6 +7,8 @@ See the file 'LICENSE' for copying permission import re +from lib.core.common import Backend +from lib.core.enums import DBMS from lib.core.enums import PRIORITY __priority__ = PRIORITY.NORMAL @@ -43,7 +45,16 @@ def _unwrapIsnull(query): break inner = retVal[match.end():end] - separator = inner.rfind(',') + separator, depth = -1, 0 # the argument separator is the comma at the top level + + for index, char in enumerate(inner): + if char == '(': + depth += 1 + elif char == ')': + depth -= 1 + elif char == ',' and depth == 0: + separator = index + if separator < 1: break @@ -75,6 +86,11 @@ def _reshape(payload, opener, tail, build): pos = pos + match.end() continue replacement = build(query, rest) + + if replacement is None: # builder declined, leave this occurrence alone + pos = pos + match.end() + continue + retVal = retVal[:start] + replacement + retVal[end + 1 + rest.end():] pos = start + len(replacement) return retVal @@ -134,7 +150,12 @@ def tamper(payload, **kwargs): def _mysqlSet(query, rest): # set-membership form of the same read ('... IN ()', used by the Huffman retrieval). # ORD('') is 0, so a past-the-end position matches the ordinal 0, which is the empty string here - position, ordinals = rest.group(1), [int(_) for _ in rest.group(2).split(',')] + position = rest.group(1) + ordinals = [int(_) for _ in rest.group(2).split(',') if _.strip().isdigit()] + + if not ordinals or any(_ > 255 for _ in ordinals): # a byte comparison cannot represent those + return None + query = _unwrapIsnull(query) members = ",".join("''" if _ == 0 else "0x%02x" % _ for _ in ordinals) return "BINARY RIGHT(LEFT(%s,%s),(%s<=LENGTH(CONVERT(%s USING ascii)))) IN (%s)" % (query, position, position, query, members) @@ -150,7 +171,9 @@ def tamper(payload, **kwargs): comma_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s*(>=|<=|>|<|=)\s*(\d+)" set_tail = r"\s*,\s*(\d+)\s*,\s*1\)\)\s+IN\s*\(([\d,\s]+)\)" - if re.search(r"(?i)IFNULL\(", payload): # also on payloads that are not single-character reads + # also on payloads that are not single-character reads. Gated on MySQL, because IFNULL() is used + # by H2, HSQLDB, Cubrid and others too, and IF() is not a function there + if Backend.getIdentifiedDbms() == DBMS.MYSQL and re.search(r"(?i)IFNULL\(", payload): payload = _unwrapIsnull(payload) retVal = _reshape(payload, r"(?i)ORD\(MID\(", set_tail, _mysqlSet)