sqlmap 的页面相似度判断

comparison

流程分析

sqlmap 的响应页面相似度的判断函数为 comparison 位于 sqlmap/lib/request/comparison.py 下。sqlmap 进行判断主要有两种方式:

  1. 如果手动设置了 stringnotStringregexpcode 等字符串比较的方式,那么 sqlmap 就会直接进行字符串匹配或者正则匹配来判断是否满足要求,只会返回 0 或者 1.
  2. 如果是默认的无配置情况,就会相对复杂,sqlmap 首先会判断掉 DBMS 错误页面或者响应异常等情况,之后主要分为两种情况。1. 是 nullConnection 的请求方式。2. 正常的请求方式:
    1. 如果是 nullConnection 的请求方式 , 即只能够拿到 content-length 页面长度。会直接拿该 content-length 长度/标准页面的 content-length 长度,获得的值即作为页面相似度。
      if kb.nullConnection and pageLength:
              if not seqMatcher.a:
                  errMsg = "problem occurred while retrieving original page content "
                  errMsg += "which prevents sqlmap from continuation. Please rerun, "
                  errMsg += "and if the problem persists turn off any optimization switches"
                  raise SqlmapNoneDataException(errMsg)
      
              ratio = 1. * pageLength / len(seqMatcher.a)
      
              if ratio > 1.:
                  ratio = 1. / ratio
      
    2. 如果是正常请求,那么 sqlmap 首先会去除掉页面中的动态内容(例如广告等)。动态内容则是在调用 checkStability 函数流程中,通过 findDynamicContent 函数对比页面内容检出的动态元素集合。接下来要确认与原始页面的比较内容即 seq1 和 seq2。
      1. 如果设置了 --titles 参数即,那么 sqlmap 接下来会只会根据 title 进行比较。sqlmap 会通过正则对响应页面进行匹配,通过 extractRegexResult 函数提取对应的 title 值作为后续的比较内容。
        # Regular expression used for extracting HTML title
        HTML_TITLE_REGEX = r"(?i)<title>(?P<result>[^<]+)</title>"
        
      2. 否则会通过 getFilteredPageContent 函数提取响应页面的纯文本内容,即去除掉 HTML 中的 script, style 和 comment 等标签内容。
        def getFilteredPageContent(page, onlyText=True, split=" "):
        """
        Returns filtered page content without script, style and/or comments
        or all HTML tags
        >>> getFilteredPageContent(u'<html><title>foobar</title><body>test</body></html>') == "foobar test"
        True
        """
        
        retVal = page
        
        # only if the page's charset has been successfully identified
        if isinstance(page, six.text_type):
            retVal = re.sub(r"(?si)<script.+?</script>|<!--.+?-->|<style.+?</style>%s" % (r"|<[^>]+>|\t|\n|\r" if onlyText else ""), split, page)
            retVal = re.sub(r"%s{2,}" % split, split, retVal)
            retVal = htmlUnescape(retVal.strip().strip(split))
        
        return retVal
        
    3. 之后会去除掉 removeReflectiveValues 函数对页面处理后可能会存在中 __REFLECTED_VALUE__ 值。
    4. 接下来如果 sqlmap 会根据 #checkStability 函数中判断该站点是一个高度动态的页面即 kb.heavilyDynamic=True 的结果来调用不同的相似度计算函数 , 并返回结果。:
      • 如果是高度动态的页面那边会调用 SequenceMatcher.ratio 函数,尝试更精准的计算相似度
      • 否则首先会计算 Hash 值,尝试在缓存 kb.cache.comparison 中查找是否有缓存。不存在才会调用 SequenceMatcher.quick_ratio 计算相似度,相对于 ration 函数计算速度会更快一点,并且对于精准度要求也没那么高。
      if conf.titles:
      	# 提取title作为比较内容
      	seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)
      	seq2 = extractRegexResult(HTML_TITLE_REGEX, page)
      else:
      	# 去除掉页面的标签内容,选取html纯文本进行比较
      	seq1 = getFilteredPageContent(seqMatcher.a, True) if conf.textOnly else seqMatcher.a
      	seq2 = getFilteredPageContent(page, True) if conf.textOnly else page
      
      if seq1 is None or seq2 is None:
      	return None
      
      seq1 = seq1.replace(REFLECTED_VALUE_MARKER, "")
      seq2 = seq2.replace(REFLECTED_VALUE_MARKER, "")
      # checkStability中如果判断是高度动态的站点
      if kb.heavilyDynamic:
      	seq1 = seq1.split("\n")
      	seq2 = seq2.split("\n")
      
      	key = None
      else:
      	key = (hash(seq1), hash(seq2))
      
      seqMatcher.set_seq1(seq1)
      seqMatcher.set_seq2(seq2)
      
      if key in kb.cache.comparison:
      	ratio = kb.cache.comparison[key]
      else:
      	ratio = round(seqMatcher.quick_ratio() if not kb.heavilyDynamic else seqMatcher.ratio(), 3)
      
      if key:
      	kb.cache.comparison[key] = ratio
      
    5. 如果最终的相似度结果在 0.02 < ratio < 0.98 之间,并且是第一次判断那么会直接将这个相似度作为基线,用来后续的相似页面进行判断,波动的幅度在 DIFF_TOLERANCE = 0.05 左右,超出则认为是存在异常。

参考链接

checkStability

当在尝试布尔盲注时,sqlmap 会首先使用 checkStability 函数对网站的动态性进行检测,提交页面相似的比较准确率。

  1. 首先 sqlmap 会再次访问这个正常的 URL,将响应内容同 初始访问时的页面 kb.originalPage 进行对比。如果页面相同,那么直接判断为是静态页面即 kb.pageStable=True
  2. 否则 sqlmap 会尝试让使用者提供 --string 或者 --regex 参数指定内容来对页面进行匹配。
  3. 如果均未指定 sqlmap 才会调用 #checkDynamicContent 函数来分析响应页面中存在的动态元素

checkDynamicContent

该函数是用来分析页面中存在动态元素的,例如广告等。

  1. 首先 sqlmap 会调用 SequenceMatcher.quick_ratio 函数来快速计算两个页面的相似度,只有在页面相似度< UPPER_RATIO_BOUND = 0.98 的情况下,会调用 #findDynamicContent 来搜索存在的动态页面内容。
  2. 之后不断的通过 Request.queryPage 请求目标 URL,然后再判断相似度,如果相似度仍 < UPPER_RATIO_BOUND = 0.98 则再次调用 #findDynamicContent 函数搜索动态内容,知道相似度>0.98 为止,并且标志该目标属于动态网站 kb.heavilyDynamic = True
  3. 如果尝试次数过多 (> conf.retries),则判断为网站过于变化过快 too dynamiac,只通过字符串进行匹配 conf.textOnly=True

findDynamicContent

sqlmap 会通过 SequenceMatcher.get_matching_blocks 来匹配两个页面中内容相同的区块,并且去除掉长度过小的区块(默认是 40,< 2*DYNAMICITY_BOUNDARY_LENGTH)。
之后会依次分析两个区块之前的内容,该内容即为动态的内容。然后取该内容中的前面/后面 DYNAMICITY_BOUNDARY_LENGTH 的内容作为 prefix/suffix,将这两个 (prefix, suffix) 作为 key 值存入 kb.dynamicMarkings 作为动态页面的标记点,后续 removeDynamicContent 在去除动态内容时则是根据该标记来去除的。