diff --git a/lib/core/agent.py b/lib/core/agent.py index 99e1b2961..b1baa7610 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -108,15 +108,7 @@ class Agent: newValue = self.cleanupPayload(newValue, origValue) - if place == PLACE.SOAP: - root = ET.XML(paramString) - iterator = root.getiterator(parameter) - - for child in iterator: - child.text = self.addPayloadDelimiters(newValue) - - retVal = ET.tostring(root) - elif place in (PLACE.URI, PLACE.CUSTOM_POST): + if place in (PLACE.URI, PLACE.CUSTOM_POST): retVal = paramString.replace("%s%s" % (origValue, CUSTOM_INJECTION_MARK_CHAR), self.addPayloadDelimiters(newValue)).replace(CUSTOM_INJECTION_MARK_CHAR, "") elif place in (PLACE.USER_AGENT, PLACE.REFERER, PLACE.HOST): retVal = paramString.replace(origValue, self.addPayloadDelimiters(newValue)) diff --git a/lib/core/common.py b/lib/core/common.py index 1ebae59a6..d339dafd1 100644 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -506,56 +506,37 @@ def paramToDict(place, parameters=None): if place in conf.parameters and not parameters: parameters = conf.parameters[place] - if place != PLACE.SOAP: - parameters = parameters.replace(", ", ",") - parameters = re.sub(r"&(\w{1,4});", r"%s\g<1>%s" % (PARAMETER_AMP_MARKER, PARAMETER_SEMICOLON_MARKER), parameters) - splitParams = parameters.split(conf.pDel or (DEFAULT_COOKIE_DELIMITER if place == PLACE.COOKIE else DEFAULT_GET_POST_DELIMITER)) + parameters = parameters.replace(", ", ",") + parameters = re.sub(r"&(\w{1,4});", r"%s\g<1>%s" % (PARAMETER_AMP_MARKER, PARAMETER_SEMICOLON_MARKER), parameters) + splitParams = parameters.split(conf.pDel or (DEFAULT_COOKIE_DELIMITER if place == PLACE.COOKIE else DEFAULT_GET_POST_DELIMITER)) - for element in splitParams: - element = re.sub(r"%s(.+?)%s" % (PARAMETER_AMP_MARKER, PARAMETER_SEMICOLON_MARKER), r"&\g<1>;", element) - elem = element.split("=") + for element in splitParams: + element = re.sub(r"%s(.+?)%s" % (PARAMETER_AMP_MARKER, PARAMETER_SEMICOLON_MARKER), r"&\g<1>;", element) + elem = element.split("=") - if len(elem) >= 2: - parameter = elem[0].replace(" ", "") - - condition = not conf.testParameter - condition |= parameter in conf.testParameter - - if condition: - testableParameters[parameter] = "=".join(elem[1:]) - if not conf.multipleTargets: - if testableParameters[parameter].strip(DUMMY_SQL_INJECTION_CHARS) != testableParameters[parameter]\ - or re.search(r'\A9{3,}', testableParameters[parameter]) or re.search(DUMMY_USER_INJECTION, testableParameters[parameter]): - warnMsg = "it appears that you have provided tainted parameter values " - warnMsg += "('%s') with most probably leftover " % element - warnMsg += "chars from manual SQL injection " - warnMsg += "tests (%s) or non-valid numerical value. " % DUMMY_SQL_INJECTION_CHARS - warnMsg += "Please, always use only valid parameter values " - warnMsg += "so sqlmap could be able to properly run " - logger.warn(warnMsg) - - message = "Are you sure you want to continue? [y/N] " - test = readInput(message, default="N") - if test[0] not in ("y", "Y"): - raise sqlmapSilentQuitException - - else: - root = ET.XML(parameters) - iterator = root.getiterator() - - for child in iterator: - parameter = child.tag - - if "}" in parameter: - testParam = parameter.split("}")[1] - else: - testParam = parameter + if len(elem) >= 2: + parameter = elem[0].replace(" ", "") condition = not conf.testParameter - condition |= testParam in conf.testParameter + condition |= parameter in conf.testParameter if condition: - testableParameters[parameter] = child.text + testableParameters[parameter] = "=".join(elem[1:]) + if not conf.multipleTargets: + if testableParameters[parameter].strip(DUMMY_SQL_INJECTION_CHARS) != testableParameters[parameter]\ + or re.search(r'\A9{3,}', testableParameters[parameter]) or re.search(DUMMY_USER_INJECTION, testableParameters[parameter]): + warnMsg = "it appears that you have provided tainted parameter values " + warnMsg += "('%s') with most probably leftover " % element + warnMsg += "chars from manual SQL injection " + warnMsg += "tests (%s) or non-valid numerical value. " % DUMMY_SQL_INJECTION_CHARS + warnMsg += "Please, always use only valid parameter values " + warnMsg += "so sqlmap could be able to properly run " + logger.warn(warnMsg) + + message = "Are you sure you want to continue? [y/N] " + test = readInput(message, default="N") + if test[0] not in ("y", "Y"): + raise sqlmapSilentQuitException if conf.testParameter and not testableParameters: paramStr = ", ".join(test for test in conf.testParameter) @@ -1992,7 +1973,7 @@ def urldecode(value, encoding=None): return result def urlencode(value, safe="%&=", convall=False, limit=False): - if conf.direct or PLACE.SOAP in conf.paramDict: + if conf.direct: return value count = 0 diff --git a/lib/core/dicts.py b/lib/core/dicts.py index ae337509f..2acc4736a 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -6,6 +6,7 @@ See the file 'doc/COPYING' for copying permission """ from lib.core.enums import DBMS +from lib.core.enums import POST_HINT from lib.core.settings import BLANK from lib.core.settings import NULL from lib.core.settings import MSSQL_ALIASES @@ -193,3 +194,8 @@ SQL_STATEMENTS = { "commit ", "rollback ", ), } + +POST_HINT_CONTENT_TYPES = { + POST_HINT.JSON: "application/json", + POST_HINT.SOAP: "application/soap+xml" + } diff --git a/lib/core/enums.py b/lib/core/enums.py index dbbefe0a3..18abc9cf7 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -58,7 +58,6 @@ class OS: class PLACE: GET = "GET" POST = "POST" - SOAP = "SOAP" URI = "URI" COOKIE = "Cookie" USER_AGENT = "User-Agent" @@ -66,6 +65,10 @@ class PLACE: HOST = "Host" CUSTOM_POST = "(custom) POST" +class POST_HINT: + SOAP = "SOAP" + JSON = "JSON" + class HTTPMETHOD: GET = "GET" POST = "POST" diff --git a/lib/core/option.py b/lib/core/option.py index ca937f347..eb3fee772 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -1508,6 +1508,7 @@ def __setKnowledgeBaseAttributes(flushAll=True): kb.pageCompress = True kb.pageTemplate = None kb.pageTemplates = dict() + kb.postHint = None kb.previousMethod = None kb.processUserMarks = None kb.orderByColumns = None diff --git a/lib/core/settings.py b/lib/core/settings.py index 663a01285..078665262 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -231,7 +231,7 @@ META_REFRESH_REGEX = r']+content="?[^">]+url=(?P< EMPTY_FORM_FIELDS_REGEX = r'(?P[^=]+=(&|\Z))' # Regular expression for soap message recognition -SOAP_REGEX = r"\A(<\?xml[^>]+>)?\s*]+>)?\s*<([^> ]+)( [^>]+)?>.+__VIEWSTATE[^"]*)[^>]+value="(?P[^"]+)' LIMITED_ROWS_TEST_NUMBER = 15 # Regular expressing used for detecting JSON-like POST data -JSON_RECOGNITION_REGEX = r'(?s)\A\s*.*"[^"]+"\s*:\s*"[^"]+".+\}\s*\Z' +JSON_RECOGNITION_REGEX = r'\A\s*\{.*"[^"]+"\s*:\s*("[^"]+"|\d+).*\}\s*\Z' + +# Default POST data content-type +DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded" diff --git a/lib/core/target.py b/lib/core/target.py index 04da9e2a9..92912cfee 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -29,6 +29,7 @@ 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.enums import POST_HINT from lib.core.exception import sqlmapFilePathException from lib.core.exception import sqlmapGenericException from lib.core.exception import sqlmapSyntaxException @@ -42,7 +43,7 @@ from lib.core.settings import HOST_ALIASES from lib.core.settings import JSON_RECOGNITION_REGEX from lib.core.settings import REFERER_ALIASES from lib.core.settings import RESULTS_FILE_FORMAT -from lib.core.settings import SOAP_REGEX +from lib.core.settings import SOAP_RECOGNITION_REGEX from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import UNENCODED_ORIGINAL_VALUE from lib.core.settings import UNICODE_ENCODING @@ -78,12 +79,35 @@ def __setRequestParams(): errMsg = "HTTP POST method depends on HTTP data value to be posted" raise sqlmapSyntaxException, errMsg - if conf.data: + if re.search(JSON_RECOGNITION_REGEX, conf.data or ""): + message = "JSON like data found in POST data. " + message += "Do you want to process it? [Y/n/q] " + test = readInput(message, default="Y") + if test and test[0] in ("q", "Q"): + raise sqlmapUserQuitException + elif test[0] not in ("n", "N"): + conf.data = re.sub(r'("[^"]+"\s*:\s*"[^"]+)"', r'\g<1>*"', conf.data) + conf.data = re.sub(r'("[^"]+"\s*:\s*)(\d+)', r'\g<1>"\g<2>*"', conf.data) + kb.processUserMarks = True + kb.postHint = POST_HINT.JSON + + elif re.search(SOAP_RECOGNITION_REGEX, conf.data or ""): + message = "SOAP like data found in POST data. " + message += "Do you want to process it? [Y/n/q] " + test = readInput(message, default="Y") + if test and test[0] in ("q", "Q"): + raise sqlmapUserQuitException + elif test[0] not in ("n", "N"): + conf.data = re.sub(r"(<([^>]+)( [^<]*)?>)([^<]+)(\g<4>*\g<5>", conf.data) + kb.processUserMarks = True + kb.postHint = POST_HINT.SOAP + + elif conf.data: if hasattr(conf.data, UNENCODED_ORIGINAL_VALUE): original = getattr(conf.data, UNENCODED_ORIGINAL_VALUE) setattr(conf.data, UNENCODED_ORIGINAL_VALUE, original) - place = PLACE.SOAP if re.match(SOAP_REGEX, conf.data, re.I | re.M) else PLACE.POST + place = PLACE.POST conf.parameters[place] = conf.data paramDict = paramToDict(place, conf.data) @@ -111,17 +135,6 @@ def __setRequestParams(): elif test[0] in ("q", "Q"): raise sqlmapUserQuitException - - if re.search(JSON_RECOGNITION_REGEX, conf.data or ""): - message = "JSON like data found in POST data. " - message += "Do you want to process it? [Y/n/q] " - test = readInput(message, default="Y") - if test and test[0] in ("q", "Q"): - raise sqlmapUserQuitException - elif test[0] not in ("n", "N"): - conf.data = re.sub(r'("[^"]+"\s*:\s*"[^"]+)"', r'\g<1>*"', conf.data or "") - kb.processUserMarks = True - for place, value in ((PLACE.URI, conf.url), (PLACE.CUSTOM_POST, conf.data)): if CUSTOM_INJECTION_MARK_CHAR in (value or ""): if kb.processUserMarks is None: diff --git a/lib/request/connect.py b/lib/request/connect.py index f0f5a133b..8d57d55af 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -6,6 +6,7 @@ See the file 'doc/COPYING' for copying permission """ import httplib +import json import re import socket import string @@ -38,17 +39,20 @@ from lib.core.common import urlencode from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger +from lib.core.dicts import POST_HINT_CONTENT_TYPES from lib.core.enums import CUSTOM_LOGGING from lib.core.enums import HTTPHEADER from lib.core.enums import HTTPMETHOD from lib.core.enums import NULLCONNECTION from lib.core.enums import PAYLOAD from lib.core.enums import PLACE +from lib.core.enums import POST_HINT from lib.core.enums import REDIRECTION from lib.core.exception import sqlmapCompressionException from lib.core.exception import sqlmapConnectionException from lib.core.exception import sqlmapSyntaxException from lib.core.settings import CUSTOM_INJECTION_MARK_CHAR +from lib.core.settings import DEFAULT_CONTENT_TYPE from lib.core.settings import HTTP_ACCEPT_HEADER_VALUE from lib.core.settings import HTTP_ACCEPT_ENCODING_HEADER_VALUE from lib.core.settings import HTTP_SILENT_TIMEOUT @@ -260,7 +264,7 @@ class Connect: requestMsg += "?%s" % get if conf.method == HTTPMETHOD.POST and not post: - for place in (PLACE.POST, PLACE.SOAP): + for place in (PLACE.POST,): if place in conf.parameters: post = conf.parameters[place] break @@ -284,6 +288,9 @@ class Connect: headers[HTTPHEADER.ACCEPT_ENCODING] = HTTP_ACCEPT_ENCODING_HEADER_VALUE if method != HTTPMETHOD.HEAD and kb.pageCompress else "identity" headers[HTTPHEADER.HOST] = host or getHostHeader(url) + if post: + headers[HTTPHEADER.CONTENT_TYPE] = POST_HINT_CONTENT_TYPES.get(kb.postHint, DEFAULT_CONTENT_TYPE) + if auxHeaders: for key, item in auxHeaders.items(): headers[key] = item @@ -308,9 +315,6 @@ class Connect: requestHeaders += "\n%s" % ("Cookie: %s" % ";".join("%s=%s" % (getUnicode(cookie.name), getUnicode(cookie.value)) for cookie in cookies)) if post: - if not getRequestHeader(req, HTTPHEADER.CONTENT_TYPE): - requestHeaders += "\n%s: %s" % (string.capwords(HTTPHEADER.CONTENT_TYPE), "application/x-www-form-urlencoded") - if not getRequestHeader(req, HTTPHEADER.CONTENT_LENGTH): requestHeaders += "\n%s: %d" % (string.capwords(HTTPHEADER.CONTENT_LENGTH), len(post)) @@ -578,10 +582,13 @@ class Connect: logger.log(CUSTOM_LOGGING.PAYLOAD, safecharencode(payload)) - if place == PLACE.SOAP: - # payloads in SOAP should have chars > and < replaced - # with their HTML encoded counterparts - payload = payload.replace('>', ">").replace('<', "<") + if place == PLACE.CUSTOM_POST: + if kb.postHint == POST_HINT.SOAP: + # payloads in SOAP should have chars > and < replaced + # with their HTML encoded counterparts + payload = payload.replace('>', ">").replace('<', "<") + elif kb.postHint == POST_HINT.JSON: + payload = json.dumps(payload)[1:-1] value = agent.replacePayload(value, payload) else: @@ -608,9 +615,6 @@ class Connect: if PLACE.CUSTOM_POST in conf.parameters: post = conf.parameters[PLACE.CUSTOM_POST].replace(CUSTOM_INJECTION_MARK_CHAR, "") if place != PLACE.CUSTOM_POST or not value else value - if PLACE.SOAP in conf.parameters: - post = conf.parameters[PLACE.SOAP] if place != PLACE.SOAP or not value else value - if PLACE.COOKIE in conf.parameters: cookie = conf.parameters[PLACE.COOKIE] if place != PLACE.COOKIE or not value else value @@ -686,9 +690,9 @@ class Connect: msg += "in this kind of situations? [Y/n]" skipUrlEncode = conf.skipUrlEncode = readInput(msg, default="Y").upper() != "N" - if place not in (PLACE.POST, PLACE.SOAP, PLACE.CUSTOM_POST) and hasattr(post, UNENCODED_ORIGINAL_VALUE): + if place not in (PLACE.POST, PLACE.CUSTOM_POST) and hasattr(post, UNENCODED_ORIGINAL_VALUE): post = getattr(post, UNENCODED_ORIGINAL_VALUE) - elif not skipUrlEncode and place not in (PLACE.SOAP,): + elif not skipUrlEncode and kb.postHint not in (POST_HINT.JSON, POST_HINT.SOAP): post = urlencode(post) if timeBasedCompare: