diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 96fdaa6f2..2c137c5f4 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -359,7 +359,7 @@ def checkSqlInjection(place, parameter, value): injectable = True - if not injectable and not conf.string and kb.pageStable: + if not injectable and not any((conf.string, conf.notString, conf.regexp)) and kb.pageStable: trueSet = set(extractTextTagContent(truePage)) falseSet = set(extractTextTagContent(falsePage)) candidates = filter(None, (_.strip() if _.strip() in (kb.pageTemplate or "") and _.strip() not in falsePage else None for _ in (trueSet - falseSet))) @@ -460,7 +460,7 @@ def checkSqlInjection(place, parameter, value): # Feed with the boundaries details only the first time a # test has been successful if injection.place is None or injection.parameter is None: - if place in (PLACE.UA, PLACE.REFERER, PLACE.HOST): + if place in (PLACE.USER_AGENT, PLACE.REFERER, PLACE.HOST): injection.parameter = place else: injection.parameter = parameter @@ -499,6 +499,7 @@ def checkSqlInjection(place, parameter, value): injection.conf.textOnly = conf.textOnly injection.conf.titles = conf.titles injection.conf.string = conf.string + injection.conf.notString = conf.notString injection.conf.regexp = conf.regexp injection.conf.optimize = conf.optimize diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 139c5847e..950a7c863 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -357,7 +357,7 @@ def start(): if (len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None)) \ and (kb.injection.place is None or kb.injection.parameter is None): - if not conf.string and not conf.regexp and PAYLOAD.TECHNIQUE.BOOLEAN in conf.tech: + if not any((conf.string, conf.notString, conf.regexp)) and PAYLOAD.TECHNIQUE.BOOLEAN in conf.tech: # NOTE: this is not needed anymore, leaving only to display # a warning message to the user in case the page is not stable checkStability() @@ -378,7 +378,7 @@ def start(): for place in parameters: # Test User-Agent and Referer headers only if # --level >= 3 - skip = (place == PLACE.UA and conf.level < 3) + skip = (place == PLACE.USER_AGENT and conf.level < 3) skip |= (place == PLACE.REFERER and conf.level < 3) # Test Host header only if @@ -388,11 +388,11 @@ def start(): # Test Cookie header only if --level >= 2 skip |= (place == PLACE.COOKIE and conf.level < 2) - skip |= (place == PLACE.UA and intersect(USER_AGENT_ALIASES, conf.skip, True) not in ([], None)) + skip |= (place == PLACE.USER_AGENT and intersect(USER_AGENT_ALIASES, conf.skip, True) not in ([], None)) skip |= (place == PLACE.REFERER and intersect(REFERER_ALIASES, conf.skip, True) not in ([], None)) skip |= (place == PLACE.COOKIE and intersect(PLACE.COOKIE, conf.skip, True) not in ([], None)) - skip &= not (place == PLACE.UA and intersect(USER_AGENT_ALIASES, conf.testParameter, True)) + skip &= not (place == PLACE.USER_AGENT and intersect(USER_AGENT_ALIASES, conf.testParameter, True)) skip &= not (place == PLACE.REFERER and intersect(REFERER_ALIASES, conf.testParameter, True)) skip &= not (place == PLACE.HOST and intersect(HOST_ALIASES, conf.testParameter, True)) @@ -527,7 +527,7 @@ def start(): errMsg += "Please, consider usage of tampering scripts as " errMsg += "your target might filter the queries." - if not conf.string and not conf.regexp: + if not conf.string and not conf.notString and not conf.regexp: errMsg += " Also, you can try to rerun by providing " errMsg += "either a valid value for option '--string' " errMsg += "(or '--regexp')" diff --git a/lib/core/agent.py b/lib/core/agent.py index 9a20a59fc..a55ae6796 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -118,7 +118,7 @@ class Agent: retVal = ET.tostring(root) elif place in (PLACE.URI, PLACE.CUSTOM_POST): retVal = paramString.replace("%s%s" % (origValue, CUSTOM_INJECTION_MARK_CHAR), self.addPayloadDelimiters(newValue)) - elif place in (PLACE.UA, PLACE.REFERER, PLACE.HOST): + elif place in (PLACE.USER_AGENT, PLACE.REFERER, PLACE.HOST): retVal = paramString.replace(origValue, self.addPayloadDelimiters(newValue)) else: retVal = paramString.replace("%s=%s" % (parameter, origValue), diff --git a/lib/core/common.py b/lib/core/common.py index 17a53a4ae..e970005c5 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -255,7 +255,7 @@ class Format: if "technology" in info: infoStr += "\nweb application technology: %s" % Format.humanize(info["technology"], ", ") - return infoStr + return infoStr.lstrip() class Backend: # Set methods @@ -2362,7 +2362,7 @@ def setOptimize(): #conf.predictOutput = True conf.keepAlive = True conf.threads = 3 if conf.threads < 3 else conf.threads - conf.nullConnection = not any([conf.data, conf.textOnly, conf.titles, conf.string, conf.regexp, conf.tor]) + conf.nullConnection = not any([conf.data, conf.textOnly, conf.titles, conf.string, conf.notString, conf.regexp, conf.tor]) if not conf.nullConnection: debugMsg = "turning off --null-connection switch used indirectly by switch -o" diff --git a/lib/core/enums.py b/lib/core/enums.py index 1f98c0490..1c0ddcd26 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -61,7 +61,7 @@ class PLACE: SOAP = "SOAP" URI = "URI" COOKIE = "Cookie" - UA = "User-Agent" + USER_AGENT = "User-Agent" REFERER = "Referer" HOST = "Host" CUSTOM_POST = "(custom) POST" diff --git a/lib/core/option.py b/lib/core/option.py index 40613fed9..2cbdc0a7e 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1867,6 +1867,14 @@ def __basicOptionValidation(): errMsg = "option '--string' is incompatible with switch '--null-connection'" raise sqlmapSyntaxException, errMsg + if conf.notString and conf.nullConnection: + errMsg = "option '--not-string' is incompatible with switch '--null-connection'" + raise sqlmapSyntaxException, errMsg + + if conf.string and conf.notString: + errMsg = "option '--string' is incompatible with switch '--not-string'" + raise sqlmapSyntaxException, errMsg + if conf.regexp and conf.nullConnection: errMsg = "option '--regexp' is incompatible with switch '--null-connection'" raise sqlmapSyntaxException, errMsg diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index ee0cbb0c9..743c97c1e 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -76,6 +76,7 @@ optDict = { "level": "integer", "risk": "integer", "string": "string", + "notString": "notString", "regexp": "string", "code": "integer", "textOnly": "boolean", @@ -87,7 +88,8 @@ optDict = { "timeSec": "integer", "uCols": "string", "uChar": "string", - "dnsName": "string" + "dnsName": "string", + "secondOrder": "string" }, "Fingerprint": { diff --git a/lib/core/target.py b/lib/core/target.py index 68f219b25..076823e45 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -26,6 +26,7 @@ from lib.core.data import logger from lib.core.data import paths from lib.core.dump import dumper from lib.core.enums import HASHDB_KEYS +from lib.core.enums import HTTPHEADER from lib.core.enums import HTTPMETHOD from lib.core.enums import PLACE from lib.core.exception import sqlmapFilePathException @@ -158,16 +159,18 @@ def __setRequestParams(): # Url encoding of the header values should be avoided # Reference: http://stackoverflow.com/questions/5085904/is-ok-to-urlencode-the-value-in-headerlocation-value - if httpHeader == PLACE.UA: - conf.parameters[PLACE.UA] = urldecode(headerValue) + httpHeader = "-".join(_.capitalize() for _ in (httpHeader or "").split("-")) + + if httpHeader == HTTPHEADER.USER_AGENT: + conf.parameters[PLACE.USER_AGENT] = urldecode(headerValue) condition = any((not conf.testParameter, intersect(conf.testParameter, USER_AGENT_ALIASES))) if condition: - conf.paramDict[PLACE.UA] = {PLACE.UA: headerValue} + conf.paramDict[PLACE.USER_AGENT] = {PLACE.USER_AGENT: headerValue} testableParameters = True - elif httpHeader == PLACE.REFERER: + elif httpHeader == HTTPHEADER.REFERER: conf.parameters[PLACE.REFERER] = urldecode(headerValue) condition = any((not conf.testParameter, intersect(conf.testParameter, REFERER_ALIASES))) @@ -176,7 +179,7 @@ def __setRequestParams(): conf.paramDict[PLACE.REFERER] = {PLACE.REFERER: headerValue} testableParameters = True - elif httpHeader == PLACE.HOST: + elif httpHeader == HTTPHEADER.HOST: conf.parameters[PLACE.HOST] = urldecode(headerValue) condition = any((not conf.testParameter, intersect(conf.testParameter, HOST_ALIASES))) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 6be188db2..82c5e7cc0 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -245,6 +245,10 @@ def cmdLineParser(): help="String to match when " "query is evaluated to True") + detection.add_option("--not-string", dest="notString", + help="String to match when " + "query is evaluated to False") + detection.add_option("--regexp", dest="regexp", help="Regexp to match when " "query is evaluated to True") @@ -284,6 +288,10 @@ def cmdLineParser(): techniques.add_option("--dns-domain", dest="dnsName", help="Domain name used for DNS exfiltration attack") + techniques.add_option("--second-order", dest="secondOrder", + help="Resulting page url searched for second-order " + "response") + # Fingerprint options fingerprint = OptionGroup(parser, "Fingerprint") diff --git a/lib/request/comparison.py b/lib/request/comparison.py index 3848e42eb..60efa0078 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -31,7 +31,7 @@ def comparison(page, headers, code=None, getRatioValue=False, pageLength=None): return _ def _adjust(condition, getRatioValue): - if not any([conf.string, conf.regexp, conf.code]): + if not any([conf.string, conf.notString, conf.regexp, conf.code]): # 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 @@ -54,14 +54,18 @@ def _comparison(page, headers, code, getRatioValue, pageLength): seqMatcher = threadData.seqMatcher seqMatcher.set_seq1(kb.pageTemplate) - if any([conf.string, conf.regexp]): + if any([conf.string, conf.notString, conf.regexp]): rawResponse = "%s%s" % (listToStrValue(headers.headers if headers else ""), page) - # String to match in page when the query is valid + # String to match in page when the query is True and/or valid if conf.string: return conf.string in rawResponse - # Regular expression to match in page when the query is valid + # String to match in page when the query is False and/or invalid + if conf.notString: + return conf.notString not in rawResponse + + # Regular expression to match in page when the query is True and/or valid if conf.regexp: return re.search(conf.regexp, rawResponse, re.I | re.M) is not None diff --git a/lib/request/connect.py b/lib/request/connect.py index 04f9b36c3..185000f98 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -192,7 +192,7 @@ class Connect: code = None page = None requestMsg = u"HTTP request [#%d]:\n%s " % (threadData.lastRequestUID, method or (HTTPMETHOD.POST if post else HTTPMETHOD.GET)) - requestMsg += "%s" % urlparse.urlsplit(url)[2] or "/" + requestMsg += ("%s" % urlparse.urlsplit(url)[2] or "/") if not any((refreshing, crawling)) else url responseMsg = u"HTTP response " requestHeaders = u"" responseHeaders = None @@ -236,7 +236,7 @@ class Connect: return page - elif any ([refreshing, crawling]): + elif any ((refreshing, crawling)): pass elif target: @@ -595,8 +595,8 @@ class Connect: if PLACE.COOKIE in conf.parameters: cookie = conf.parameters[PLACE.COOKIE] if place != PLACE.COOKIE or not value else value - if PLACE.UA in conf.parameters: - ua = conf.parameters[PLACE.UA] if place != PLACE.UA or not value else value + if PLACE.USER_AGENT in conf.parameters: + ua = conf.parameters[PLACE.USER_AGENT] if place != PLACE.USER_AGENT or not value else value if PLACE.REFERER in conf.parameters: referer = conf.parameters[PLACE.REFERER] if place != PLACE.REFERER or not value else value @@ -731,6 +731,9 @@ class Connect: if not pageLength: page, headers, code = Connect.getPage(url=uri, get=get, post=post, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, method=method, auxHeaders=auxHeaders, response=response, raise404=raise404, ignoreTimeout=timeBasedCompare) + if conf.secondOrder: + page, headers, code = Connect.getPage(url=conf.secondOrder, cookie=cookie, ua=ua, silent=silent, auxHeaders=auxHeaders, response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True) + threadData.lastQueryDuration = calculateDeltaSeconds(start) kb.originalCode = kb.originalCode or code diff --git a/lib/utils/hashdb.py b/lib/utils/hashdb.py index d906939b8..a2ae4cda4 100644 --- a/lib/utils/hashdb.py +++ b/lib/utils/hashdb.py @@ -6,6 +6,7 @@ See the file 'doc/COPYING' for copying permission """ import hashlib +import os import sqlite3 import threading import time @@ -54,7 +55,7 @@ class HashDB(object): def retrieve(self, key, unserialize=False): retVal = None - if key: + if key and (self._write_cache or os.path.isfile(self.filepath)): hash_ = HashDB.hashKey(key) retVal = self._write_cache.get(hash_, None) if not retVal: diff --git a/sqlmap.conf b/sqlmap.conf index 85ea6e9ed..135e3dd9f 100644 --- a/sqlmap.conf +++ b/sqlmap.conf @@ -247,6 +247,11 @@ risk = 1 # Refer to the user's manual for further details. string = +# String to match within the raw response when the query is evaluated to +# False, only needed if the page content dynamically changes at each refresh. +# Refer to the user's manual for further details. +notString = + # Regular expression to match within the raw response when the query is # evaluated to True, only needed if the needed if the page content # dynamically changes at each refresh. @@ -305,6 +310,10 @@ uChar = # Valid: string dnsName = +# Resulting page url searched for second-order response +# Valid: string +secondOrder = + [Fingerprint]