#!/usr/bin/env python """ Copyright (c) 2006-2018 sqlmap developers (http://sqlmap.org/) See the file 'LICENSE' for copying permission """ import os import re import time from lib.controller.action import action from lib.controller.checks import checkSqlInjection from lib.controller.checks import checkDynParam from lib.controller.checks import checkStability from lib.controller.checks import checkString from lib.controller.checks import checkRegexp from lib.controller.checks import checkConnection from lib.controller.checks import checkInternet from lib.controller.checks import checkNullConnection from lib.controller.checks import checkWaf from lib.controller.checks import heuristicCheckSqlInjection from lib.controller.checks import identifyWaf from lib.core.agent import agent from lib.core.common import dataToStdout from lib.core.common import extractRegexResult from lib.core.common import getFilteredPageContent from lib.core.common import getPublicTypeMembers from lib.core.common import getSafeExString from lib.core.common import hashDBRetrieve from lib.core.common import hashDBWrite from lib.core.common import intersect from lib.core.common import isListLike from lib.core.common import parseTargetUrl from lib.core.common import popValue from lib.core.common import pushValue from lib.core.common import randomStr from lib.core.common import readInput from lib.core.common import safeCSValue from lib.core.common import showHttpErrorCodes from lib.core.common import urlencode from lib.core.common import urldecode from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.decorators import stackedmethod from lib.core.enums import CONTENT_TYPE from lib.core.enums import HASHDB_KEYS from lib.core.enums import HEURISTIC_TEST from lib.core.enums import HTTPMETHOD from lib.core.enums import NOTE from lib.core.enums import PAYLOAD from lib.core.enums import PLACE from lib.core.exception import SqlmapBaseException from lib.core.exception import SqlmapNoneDataException from lib.core.exception import SqlmapNotVulnerableException from lib.core.exception import SqlmapSilentQuitException from lib.core.exception import SqlmapSkipTargetException from lib.core.exception import SqlmapSystemException from lib.core.exception import SqlmapValueException from lib.core.exception import SqlmapUserQuitException from lib.core.settings import ASP_NET_CONTROL_REGEX from lib.core.settings import CSRF_TOKEN_PARAMETER_INFIXES from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import EMPTY_FORM_FIELDS_REGEX from lib.core.settings import IGNORE_PARAMETERS from lib.core.settings import LOW_TEXT_PERCENT from lib.core.settings import GOOGLE_ANALYTICS_COOKIE_PREFIX from lib.core.settings import HOST_ALIASES from lib.core.settings import REFERER_ALIASES from lib.core.settings import USER_AGENT_ALIASES from lib.core.target import initTargetEnv from lib.core.target import setupTargetEnv def _selectInjection(): """ Selection function for injection place, parameters and type. """ points = {} for injection in kb.injections: place = injection.place parameter = injection.parameter ptype = injection.ptype point = (place, parameter, ptype) if point not in points: points[point] = injection else: for key in points[point].keys(): if key != 'data': points[point][key] = points[point][key] or injection[key] points[point]['data'].update(injection['data']) if len(points) == 1: kb.injection = kb.injections[0] elif len(points) > 1: message = "there were multiple injection points, please select " message += "the one to use for following injections:\n" points = [] for i in xrange(0, len(kb.injections)): place = kb.injections[i].place parameter = kb.injections[i].parameter ptype = kb.injections[i].ptype point = (place, parameter, ptype) if point not in points: points.append(point) ptype = PAYLOAD.PARAMETER[ptype] if isinstance(ptype, int) else ptype message += "[%d] place: %s, parameter: " % (i, place) message += "%s, type: %s" % (parameter, ptype) if i == 0: message += " (default)" message += "\n" message += "[q] Quit" choice = readInput(message, default='0').upper() if choice.isdigit() and int(choice) < len(kb.injections) and int(choice) >= 0: index = int(choice) elif choice == 'Q': raise SqlmapUserQuitException else: errMsg = "invalid choice" raise SqlmapValueException(errMsg) kb.injection = kb.injections[index] def _formatInjection(inj): paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else inj.place data = "Parameter: %s (%s)\n" % (inj.parameter, paramType) for stype, sdata in inj.data.items(): title = sdata.title vector = sdata.vector comment = sdata.comment payload = agent.adjustLateValues(sdata.payload) if inj.place == PLACE.CUSTOM_HEADER: payload = payload.split(',', 1)[1] if stype == PAYLOAD.TECHNIQUE.UNION: count = re.sub(r"(?i)(\(.+\))|(\blimit[^a-z]+)", "", sdata.payload).count(',') + 1 title = re.sub(r"\d+ to \d+", str(count), title) vector = agent.forgeUnionQuery("[QUERY]", vector[0], vector[1], vector[2], None, None, vector[5], vector[6]) if count == 1: title = title.replace("columns", "column") elif comment: vector = "%s%s" % (vector, comment) data += " Type: %s\n" % PAYLOAD.SQLINJECTION[stype] data += " Title: %s\n" % title data += " Payload: %s\n" % urldecode(payload, unsafe="&", spaceplus=(inj.place != PLACE.GET and kb.postSpaceToPlus)) data += " Vector: %s\n\n" % vector if conf.verbose > 1 else "\n" return data def _showInjections(): if conf.wizard and kb.wizardMode: kb.wizardMode = False if kb.testQueryCount > 0: header = "sqlmap identified the following injection point(s) with " header += "a total of %d HTTP(s) requests" % kb.testQueryCount else: header = "sqlmap resumed the following injection point(s) from stored session" if conf.api: conf.dumper.string("", {"url": conf.url, "query": conf.parameters.get(PLACE.GET), "data": conf.parameters.get(PLACE.POST)}, content_type=CONTENT_TYPE.TARGET) conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES) else: data = "".join(set(_formatInjection(_) for _ in kb.injections)).rstrip("\n") conf.dumper.string(header, data) if conf.tamper: warnMsg = "changes made by tampering scripts are not " warnMsg += "included in shown payload content(s)" logger.warn(warnMsg) if conf.hpp: warnMsg = "changes made by HTTP parameter pollution are not " warnMsg += "included in shown payload content(s)" logger.warn(warnMsg) def _randomFillBlankFields(value): retVal = value if extractRegexResult(EMPTY_FORM_FIELDS_REGEX, value): message = "do you want to fill blank fields with random values? [Y/n] " if readInput(message, default='Y', boolean=True): for match in re.finditer(EMPTY_FORM_FIELDS_REGEX, retVal): item = match.group("result") if not any(_ in item for _ in IGNORE_PARAMETERS) and not re.search(ASP_NET_CONTROL_REGEX, item): if item[-1] == DEFAULT_GET_POST_DELIMITER: retVal = retVal.replace(item, "%s%s%s" % (item[:-1], randomStr(), DEFAULT_GET_POST_DELIMITER)) else: retVal = retVal.replace(item, "%s%s" % (item, randomStr())) return retVal def _saveToHashDB(): injections = hashDBRetrieve(HASHDB_KEYS.KB_INJECTIONS, True) if not isListLike(injections): injections = [] injections.extend(_ for _ in kb.injections if _ and _.place is not None and _.parameter is not None) _ = dict() for injection in injections: key = (injection.place, injection.parameter, injection.ptype) if key not in _: _[key] = injection else: _[key].data.update(injection.data) hashDBWrite(HASHDB_KEYS.KB_INJECTIONS, _.values(), True) _ = hashDBRetrieve(HASHDB_KEYS.KB_ABS_FILE_PATHS, True) hashDBWrite(HASHDB_KEYS.KB_ABS_FILE_PATHS, kb.absFilePaths | (_ if isinstance(_, set) else set()), True) if not hashDBRetrieve(HASHDB_KEYS.KB_CHARS): hashDBWrite(HASHDB_KEYS.KB_CHARS, kb.chars, True) if not hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS): hashDBWrite(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, kb.dynamicMarkings, True) def _saveToResultsFile(): if not conf.resultsFP: return results = {} techniques = dict((_[1], _[0]) for _ in getPublicTypeMembers(PAYLOAD.TECHNIQUE)) for injection in kb.injections + kb.falsePositives: if injection.place is None or injection.parameter is None: continue key = (injection.place, injection.parameter, ';'.join(injection.notes)) if key not in results: results[key] = [] results[key].extend(injection.data.keys()) try: for key, value in results.items(): place, parameter, notes = key line = "%s,%s,%s,%s,%s%s" % (safeCSValue(kb.originalUrls.get(conf.url) or conf.url), place, parameter, "".join(techniques[_][0].upper() for _ in sorted(value)), notes, os.linesep) conf.resultsFP.write(line) if not results: line = "%s,,,,%s" % (conf.url, os.linesep) conf.resultsFP.write(line) conf.resultsFP.flush() except IOError, ex: errMsg = "unable to write to the results file '%s' ('%s'). " % (conf.resultsFilename, getSafeExString(ex)) raise SqlmapSystemException(errMsg) @stackedmethod def start(): """ This function calls a function that performs checks on both URL stability and all GET, POST, Cookie and User-Agent parameters to check if they are dynamic and SQL injection affected """ if conf.direct: initTargetEnv() setupTargetEnv() action() return True if conf.url and not any((conf.forms, conf.crawlDepth)): kb.targets.add((conf.url, conf.method, conf.data, conf.cookie, None)) if conf.configFile and not kb.targets: errMsg = "you did not edit the configuration file properly, set " errMsg += "the target URL, list of targets or google dork" logger.error(errMsg) return False if kb.targets and len(kb.targets) > 1: infoMsg = "sqlmap got a total of %d targets" % len(kb.targets) logger.info(infoMsg) hostCount = 0 initialHeaders = list(conf.httpHeaders) for targetUrl, targetMethod, targetData, targetCookie, targetHeaders in kb.targets: try: if conf.checkInternet: infoMsg = "checking for Internet connection" logger.info(infoMsg) if not checkInternet(): warnMsg = "[%s] [WARNING] no connection detected" % time.strftime("%X") dataToStdout(warnMsg) while not checkInternet(): dataToStdout('.') time.sleep(5) dataToStdout("\n") conf.url = targetUrl conf.method = targetMethod.upper() if targetMethod else targetMethod conf.data = targetData conf.cookie = targetCookie conf.httpHeaders = list(initialHeaders) conf.httpHeaders.extend(targetHeaders or []) conf.httpHeaders = [conf.httpHeaders[i] for i in xrange(len(conf.httpHeaders)) if conf.httpHeaders[i][0].upper() not in (__[0].upper() for __ in conf.httpHeaders[i + 1:])] initTargetEnv() parseTargetUrl() testSqlInj = False if PLACE.GET in conf.parameters and not any([conf.data, conf.testParameter]): for parameter in re.findall(r"([^=]+)=([^%s]+%s?|\Z)" % (re.escape(conf.paramDel or "") or DEFAULT_GET_POST_DELIMITER, re.escape(conf.paramDel or "") or DEFAULT_GET_POST_DELIMITER), conf.parameters[PLACE.GET]): paramKey = (conf.hostname, conf.path, PLACE.GET, parameter[0]) if paramKey not in kb.testedParams: testSqlInj = True break else: paramKey = (conf.hostname, conf.path, None, None) if paramKey not in kb.testedParams: testSqlInj = True if testSqlInj and conf.hostname in kb.vulnHosts: if kb.skipVulnHost is None: message = "SQL injection vulnerability has already been detected " message += "against '%s'. Do you want to skip " % conf.hostname message += "further tests involving it? [Y/n]" kb.skipVulnHost = readInput(message, default='Y', boolean=True) testSqlInj = not kb.skipVulnHost if not testSqlInj: infoMsg = "skipping '%s'" % targetUrl logger.info(infoMsg) continue if conf.multipleTargets: hostCount += 1 if conf.forms and conf.method: message = "[#%d] form:\n%s %s" % (hostCount, conf.method, targetUrl) else: message = "URL %d:\n%s %s" % (hostCount, HTTPMETHOD.GET, targetUrl) if conf.cookie: message += "\nCookie: %s" % conf.cookie if conf.data is not None: message += "\n%s data: %s" % ((conf.method if conf.method != HTTPMETHOD.GET else conf.method) or HTTPMETHOD.POST, urlencode(conf.data) if conf.data else "") if conf.forms and conf.method: if conf.method == HTTPMETHOD.GET and targetUrl.find("?") == -1: continue message += "\ndo you want to test this form? [Y/n/q] " choice = readInput(message, default='Y').upper() if choice == 'N': continue elif choice == 'Q': break else: if conf.method != HTTPMETHOD.GET: message = "Edit %s data [default: %s]%s: " % (conf.method, urlencode(conf.data) if conf.data else "None", " (Warning: blank fields detected)" if conf.data and extractRegexResult(EMPTY_FORM_FIELDS_REGEX, conf.data) else "") conf.data = readInput(message, default=conf.data) conf.data = _randomFillBlankFields(conf.data) conf.data = urldecode(conf.data) if conf.data and urlencode(DEFAULT_GET_POST_DELIMITER, None) not in conf.data else conf.data else: if '?' in targetUrl: firstPart, secondPart = targetUrl.split('?', 1) message = "Edit GET data [default: %s]: " % secondPart test = readInput(message, default=secondPart) test = _randomFillBlankFields(test) conf.url = "%s?%s" % (firstPart, test) parseTargetUrl() else: message += "\ndo you want to test this URL? [Y/n/q]" choice = readInput(message, default='Y').upper() if choice == 'N': dataToStdout(os.linesep) continue elif choice == 'Q': break infoMsg = "testing URL '%s'" % targetUrl logger.info(infoMsg) setupTargetEnv() if not checkConnection(suppressOutput=conf.forms) or not checkString() or not checkRegexp(): continue checkWaf() if conf.identifyWaf: identifyWaf() if conf.nullConnection: checkNullConnection() 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 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() # Do a little prioritization reorder of a testable parameter list parameters = conf.parameters.keys() # Order of testing list (first to last) orderList = (PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER, PLACE.URI, PLACE.POST, PLACE.GET) for place in orderList[::-1]: if place in parameters: parameters.remove(place) parameters.insert(0, place) proceed = True for place in parameters: # Test User-Agent and Referer headers only if # --level >= 3 skip = (place == PLACE.USER_AGENT and conf.level < 3) skip |= (place == PLACE.REFERER and conf.level < 3) # Test Host header only if # --level >= 5 skip |= (place == PLACE.HOST and conf.level < 5) # Test Cookie header only if --level >= 2 skip |= (place == PLACE.COOKIE and conf.level < 2) 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 |= (place == PLACE.HOST and intersect(PLACE.HOST, conf.skip, True) not in ([], None)) 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)) skip &= not (place == PLACE.COOKIE and intersect((PLACE.COOKIE,), conf.testParameter, True)) if skip: continue if kb.testOnlyCustom and place not in (PLACE.URI, PLACE.CUSTOM_POST, PLACE.CUSTOM_HEADER): continue if place not in conf.paramDict: continue paramDict = conf.paramDict[place] paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place for parameter, value in paramDict.items(): if not proceed: break kb.vainRun = False testSqlInj = True paramKey = (conf.hostname, conf.path, place, parameter) if paramKey in kb.testedParams: testSqlInj = False infoMsg = "skipping previously processed %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) elif parameter in conf.testParameter: pass elif parameter == conf.rParam: testSqlInj = False infoMsg = "skipping randomizing %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) elif parameter in conf.skip or kb.postHint and parameter.split(' ')[-1] in conf.skip: testSqlInj = False infoMsg = "skipping %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) elif conf.paramExclude and (re.search(conf.paramExclude, parameter, re.I) or kb.postHint and re.search(conf.paramExclude, parameter.split(' ')[-1], re.I)): testSqlInj = False infoMsg = "skipping %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) elif conf.csrfToken and re.search(conf.csrfToken, parameter, re.I): testSqlInj = False infoMsg = "skipping anti-CSRF token parameter '%s'" % parameter logger.info(infoMsg) # Ignore session-like parameters for --level < 4 elif conf.level < 4 and (parameter.upper() in IGNORE_PARAMETERS or any(_ in parameter.lower() for _ in CSRF_TOKEN_PARAMETER_INFIXES) or parameter.upper().startswith(GOOGLE_ANALYTICS_COOKIE_PREFIX)): testSqlInj = False infoMsg = "ignoring %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) elif PAYLOAD.TECHNIQUE.BOOLEAN in conf.tech or conf.skipStatic: check = checkDynParam(place, parameter, value) if not check: warnMsg = "%s parameter '%s' does not appear to be dynamic" % (paramType, parameter) logger.warn(warnMsg) if conf.skipStatic: infoMsg = "skipping static %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) testSqlInj = False else: infoMsg = "%s parameter '%s' appears to be dynamic" % (paramType, parameter) logger.info(infoMsg) kb.testedParams.add(paramKey) if testSqlInj: try: if place == PLACE.COOKIE: pushValue(kb.mergeCookies) kb.mergeCookies = False check = heuristicCheckSqlInjection(place, parameter) if check != HEURISTIC_TEST.POSITIVE: if conf.smart or (kb.ignoreCasted and check == HEURISTIC_TEST.CASTED): infoMsg = "skipping %s parameter '%s'" % (paramType, parameter) logger.info(infoMsg) continue infoMsg = "testing for SQL injection on %s " % paramType infoMsg += "parameter '%s'" % parameter logger.info(infoMsg) injection = checkSqlInjection(place, parameter, value) proceed = not kb.endDetection injectable = False if getattr(injection, "place", None) is not None: if NOTE.FALSE_POSITIVE_OR_UNEXPLOITABLE in injection.notes: kb.falsePositives.append(injection) else: injectable = True kb.injections.append(injection) # In case when user wants to end detection phase (Ctrl+C) if not proceed: break msg = "%s parameter '%s' " % (injection.place, injection.parameter) msg += "is vulnerable. Do you want to keep testing the others (if any)? [y/N] " if not readInput(msg, default='N', boolean=True): proceed = False paramKey = (conf.hostname, conf.path, None, None) kb.testedParams.add(paramKey) if not injectable: warnMsg = "%s parameter '%s' does not seem to be " % (paramType, parameter) warnMsg += "injectable" logger.warn(warnMsg) finally: if place == PLACE.COOKIE: kb.mergeCookies = popValue() if len(kb.injections) == 0 or (len(kb.injections) == 1 and kb.injections[0].place is None): if kb.vainRun and not conf.multipleTargets: errMsg = "no parameter(s) found for testing in the provided data " errMsg += "(e.g. GET parameter 'id' in 'www.site.com/index.php?id=1')" raise SqlmapNoneDataException(errMsg) else: errMsg = "all tested parameters do not appear to be injectable." if conf.level < 5 or conf.risk < 3: errMsg += " Try to increase values for '--level'/'--risk' options " errMsg += "if you wish to perform more tests." if isinstance(conf.tech, list) and len(conf.tech) < 5: errMsg += " Rerun without providing the option '--technique'." if not conf.textOnly and kb.originalPage: percent = (100.0 * len(getFilteredPageContent(kb.originalPage)) / len(kb.originalPage)) if kb.dynamicMarkings: errMsg += " You can give it a go with the switch '--text-only' " errMsg += "if the target page has a low percentage " errMsg += "of textual content (~%.2f%% of " % percent errMsg += "page content is text)." elif percent < LOW_TEXT_PERCENT and not kb.errorIsNone: errMsg += " Please retry with the switch '--text-only' " errMsg += "(along with --technique=BU) as this case " errMsg += "looks like a perfect candidate " errMsg += "(low textual content along with inability " errMsg += "of comparison engine to detect at least " errMsg += "one dynamic parameter)." if kb.heuristicTest == HEURISTIC_TEST.POSITIVE: errMsg += " As heuristic test turned out positive you are " errMsg += "strongly advised to continue on with the tests." if conf.string: errMsg += " Also, you can try to rerun by providing a " errMsg += "valid value for option '--string' as perhaps the string you " errMsg += "have chosen does not match " errMsg += "exclusively True responses." elif conf.regexp: errMsg += " Also, you can try to rerun by providing a " errMsg += "valid value for option '--regexp' as perhaps the regular " errMsg += "expression that you have chosen " errMsg += "does not match exclusively True responses." if not conf.tamper: errMsg += " If you suspect that there is some kind of protection mechanism " errMsg += "involved (e.g. WAF) maybe you could try to use " errMsg += "option '--tamper' (e.g. '--tamper=space2comment')" raise SqlmapNotVulnerableException(errMsg.rstrip('.')) else: # Flush the flag kb.testMode = False _saveToResultsFile() _saveToHashDB() _showInjections() _selectInjection() if kb.injection.place is not None and kb.injection.parameter is not None: if conf.multipleTargets: message = "do you want to exploit this SQL injection? [Y/n] " condition = readInput(message, default='Y', boolean=True) else: condition = True if condition: action() except KeyboardInterrupt: if conf.multipleTargets: warnMsg = "user aborted in multiple target mode" logger.warn(warnMsg) message = "do you want to skip to the next target in list? [Y/n/q]" choice = readInput(message, default='Y').upper() if choice == 'N': return False elif choice == 'Q': raise SqlmapUserQuitException else: raise except SqlmapSkipTargetException: pass except SqlmapUserQuitException: raise except SqlmapSilentQuitException: raise except SqlmapBaseException, ex: errMsg = getSafeExString(ex) if conf.multipleTargets: _saveToResultsFile() errMsg += ", skipping to the next %s" % ("form" if conf.forms else "URL") logger.error(errMsg.lstrip(", ")) else: logger.critical(errMsg) return False finally: showHttpErrorCodes() if kb.maxConnectionsFlag: warnMsg = "it appears that the target " warnMsg += "has a maximum connections " warnMsg += "constraint" logger.warn(warnMsg) if kb.dataOutputFlag and not conf.multipleTargets: logger.info("fetched data logged to text files under '%s'" % conf.outputPath) if conf.multipleTargets: if conf.resultsFilename: infoMsg = "you can find results of scanning in multiple targets " infoMsg += "mode inside the CSV file '%s'" % conf.resultsFilename logger.info(infoMsg) return True