add comments to request

pull/3/head
wang 3 months ago
parent 129d83dfba
commit 33945c2dbd

@ -1,8 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
pass

@ -1,446 +0,0 @@
#!/usr/bin/env python
"""
Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
import codecs
import gzip
import io
import logging
import re
import struct
import zlib
from lib.core.common import Backend
from lib.core.common import extractErrorMessage
from lib.core.common import extractRegexResult
from lib.core.common import filterNone
from lib.core.common import getPublicTypeMembers
from lib.core.common import getSafeExString
from lib.core.common import isListLike
from lib.core.common import randomStr
from lib.core.common import readInput
from lib.core.common import resetCookieJar
from lib.core.common import singleTimeLogMessage
from lib.core.common import singleTimeWarnMessage
from lib.core.common import unArrayizeValue
from lib.core.convert import decodeHex
from lib.core.convert import getBytes
from lib.core.convert import getText
from lib.core.convert import getUnicode
from lib.core.data import conf
from lib.core.data import kb
from lib.core.data import logger
from lib.core.decorators import cachedmethod
from lib.core.decorators import lockedmethod
from lib.core.dicts import HTML_ENTITIES
from lib.core.enums import DBMS
from lib.core.enums import HTTP_HEADER
from lib.core.enums import PLACE
from lib.core.exception import SqlmapCompressionException
from lib.core.settings import BLOCKED_IP_REGEX
from lib.core.settings import DEFAULT_COOKIE_DELIMITER
from lib.core.settings import EVENTVALIDATION_REGEX
from lib.core.settings import HEURISTIC_PAGE_SIZE_THRESHOLD
from lib.core.settings import IDENTYWAF_PARSE_LIMIT
from lib.core.settings import MAX_CONNECTION_TOTAL_SIZE
from lib.core.settings import META_CHARSET_REGEX
from lib.core.settings import PARSE_HEADERS_LIMIT
from lib.core.settings import PRINTABLE_BYTES
from lib.core.settings import SELECT_FROM_TABLE_REGEX
from lib.core.settings import UNICODE_ENCODING
from lib.core.settings import VIEWSTATE_REGEX
from lib.parse.headers import headersParser
from lib.parse.html import htmlParser
from thirdparty import six
from thirdparty.chardet import detect
from thirdparty.identywaf import identYwaf
from thirdparty.odict import OrderedDict
from thirdparty.six import unichr as _unichr
from thirdparty.six.moves import http_client as _http_client
@lockedmethod
def forgeHeaders(items=None, base=None):
"""
Prepare HTTP Cookie, HTTP User-Agent and HTTP Referer headers to use when performing
the HTTP requests
"""
items = items or {}
for _ in list(items.keys()):
if items[_] is None:
del items[_]
headers = OrderedDict(conf.httpHeaders if base is None else base)
headers.update(items.items())
class _str(str):
def capitalize(self):
return _str(self)
def title(self):
return _str(self)
_ = headers
headers = OrderedDict()
for key, value in _.items():
success = False
for _ in headers:
if _.upper() == key.upper():
del headers[_]
break
if key.upper() not in (_.upper() for _ in getPublicTypeMembers(HTTP_HEADER, True)):
try:
headers[_str(key)] = value # dirty hack for http://bugs.python.org/issue12455
except UnicodeEncodeError: # don't do the hack on non-ASCII header names (they have to be properly encoded later on)
pass
else:
success = True
if not success:
key = '-'.join(_.capitalize() for _ in key.split('-'))
headers[key] = value
if conf.cj:
if HTTP_HEADER.COOKIE in headers:
for cookie in conf.cj:
if cookie is None or cookie.domain_specified and not (conf.hostname or "").endswith(cookie.domain):
continue
if ("%s=" % getUnicode(cookie.name)) in getUnicode(headers[HTTP_HEADER.COOKIE]):
if conf.loadCookies:
conf.httpHeaders = filterNone((item if item[0] != HTTP_HEADER.COOKIE else None) for item in conf.httpHeaders)
elif kb.mergeCookies is None:
message = "you provided a HTTP %s header value, while " % HTTP_HEADER.COOKIE
message += "target URL provides its own cookies within "
message += "HTTP %s header which intersect with yours. " % HTTP_HEADER.SET_COOKIE
message += "Do you want to merge them in further requests? [Y/n] "
kb.mergeCookies = readInput(message, default='Y', boolean=True)
if kb.mergeCookies and kb.injection.place != PLACE.COOKIE:
def _(value):
return re.sub(r"(?i)\b%s=[^%s]+" % (re.escape(getUnicode(cookie.name)), conf.cookieDel or DEFAULT_COOKIE_DELIMITER), ("%s=%s" % (getUnicode(cookie.name), getUnicode(cookie.value))).replace('\\', r'\\'), value)
headers[HTTP_HEADER.COOKIE] = _(headers[HTTP_HEADER.COOKIE])
if PLACE.COOKIE in conf.parameters:
conf.parameters[PLACE.COOKIE] = _(conf.parameters[PLACE.COOKIE])
conf.httpHeaders = [(item[0], item[1] if item[0] != HTTP_HEADER.COOKIE else _(item[1])) for item in conf.httpHeaders]
elif not kb.testMode:
headers[HTTP_HEADER.COOKIE] += "%s %s=%s" % (conf.cookieDel or DEFAULT_COOKIE_DELIMITER, getUnicode(cookie.name), getUnicode(cookie.value))
if kb.testMode and not any((conf.csrfToken, conf.safeUrl)):
resetCookieJar(conf.cj)
return headers
def parseResponse(page, headers, status=None):
"""
@param page: the page to parse to feed the knowledge base htmlFp
(back-end DBMS fingerprint based upon DBMS error messages return
through the web application) list and absFilePaths (absolute file
paths) set.
"""
if headers:
headersParser(headers)
if page:
htmlParser(page if not status else "%s\n\n%s" % (status, page))
@cachedmethod
def checkCharEncoding(encoding, warn=True):
"""
Checks encoding name, repairs common misspellings and adjusts to
proper namings used in codecs module
>>> checkCharEncoding('iso-8858', False)
'iso8859-1'
>>> checkCharEncoding('en_us', False)
'utf8'
"""
if isinstance(encoding, six.binary_type):
encoding = getUnicode(encoding)
if isListLike(encoding):
encoding = unArrayizeValue(encoding)
if encoding:
encoding = encoding.lower()
else:
return encoding
# Reference: http://www.destructor.de/charsets/index.htm
translate = {"windows-874": "iso-8859-11", "utf-8859-1": "utf8", "en_us": "utf8", "macintosh": "iso-8859-1", "euc_tw": "big5_tw", "th": "tis-620", "unicode": "utf8", "utc8": "utf8", "ebcdic": "ebcdic-cp-be", "iso-8859": "iso8859-1", "iso-8859-0": "iso8859-1", "ansi": "ascii", "gbk2312": "gbk", "windows-31j": "cp932", "en": "us"}
for delimiter in (';', ',', '('):
if delimiter in encoding:
encoding = encoding[:encoding.find(delimiter)].strip()
encoding = encoding.replace("&quot", "")
# popular typos/errors
if "8858" in encoding:
encoding = encoding.replace("8858", "8859") # iso-8858 -> iso-8859
elif "8559" in encoding:
encoding = encoding.replace("8559", "8859") # iso-8559 -> iso-8859
elif "8895" in encoding:
encoding = encoding.replace("8895", "8859") # iso-8895 -> iso-8859
elif "5889" in encoding:
encoding = encoding.replace("5889", "8859") # iso-5889 -> iso-8859
elif "5589" in encoding:
encoding = encoding.replace("5589", "8859") # iso-5589 -> iso-8859
elif "2313" in encoding:
encoding = encoding.replace("2313", "2312") # gb2313 -> gb2312
elif encoding.startswith("x-"):
encoding = encoding[len("x-"):] # x-euc-kr -> euc-kr / x-mac-turkish -> mac-turkish
elif "windows-cp" in encoding:
encoding = encoding.replace("windows-cp", "windows") # windows-cp-1254 -> windows-1254
# name adjustment for compatibility
if encoding.startswith("8859"):
encoding = "iso-%s" % encoding
elif encoding.startswith("cp-"):
encoding = "cp%s" % encoding[3:]
elif encoding.startswith("euc-"):
encoding = "euc_%s" % encoding[4:]
elif encoding.startswith("windows") and not encoding.startswith("windows-"):
encoding = "windows-%s" % encoding[7:]
elif encoding.find("iso-88") > 0:
encoding = encoding[encoding.find("iso-88"):]
elif encoding.startswith("is0-"):
encoding = "iso%s" % encoding[4:]
elif encoding.find("ascii") > 0:
encoding = "ascii"
elif encoding.find("utf8") > 0:
encoding = "utf8"
elif encoding.find("utf-8") > 0:
encoding = "utf-8"
# Reference: http://philip.html5.org/data/charsets-2.html
if encoding in translate:
encoding = translate[encoding]
elif encoding in ("null", "{charset}", "charset", "*") or not re.search(r"\w", encoding):
return None
# Reference: http://www.iana.org/assignments/character-sets
# Reference: http://docs.python.org/library/codecs.html
try:
codecs.lookup(encoding)
except:
encoding = None
if encoding:
try:
six.text_type(getBytes(randomStr()), encoding)
except:
if warn:
warnMsg = "invalid web page charset '%s'" % encoding
singleTimeLogMessage(warnMsg, logging.WARN, encoding)
encoding = None
return encoding
def getHeuristicCharEncoding(page):
"""
Returns page encoding charset detected by usage of heuristics
Reference: https://chardet.readthedocs.io/en/latest/usage.html
>>> getHeuristicCharEncoding(b"<html></html>")
'ascii'
"""
key = hash(page)
retVal = kb.cache.encoding[key] if key in kb.cache.encoding else detect(page[:HEURISTIC_PAGE_SIZE_THRESHOLD])["encoding"]
kb.cache.encoding[key] = retVal
if retVal and retVal.lower().replace('-', "") == UNICODE_ENCODING.lower().replace('-', ""):
infoMsg = "heuristics detected web page charset '%s'" % retVal
singleTimeLogMessage(infoMsg, logging.INFO, retVal)
return retVal
def decodePage(page, contentEncoding, contentType, percentDecode=True):
"""
Decode compressed/charset HTTP response
>>> getText(decodePage(b"<html>foo&amp;bar</html>", None, "text/html; charset=utf-8"))
'<html>foo&bar</html>'
>>> getText(decodePage(b"&#x9;", None, "text/html; charset=utf-8"))
'\\t'
"""
if not page or (conf.nullConnection and len(page) < 2):
return getUnicode(page)
contentEncoding = contentEncoding.lower() if hasattr(contentEncoding, "lower") else ""
contentType = contentType.lower() if hasattr(contentType, "lower") else ""
if contentEncoding in ("gzip", "x-gzip", "deflate"):
if not kb.pageCompress:
return None
try:
if contentEncoding == "deflate":
data = io.BytesIO(zlib.decompress(page, -15)) # Reference: http://stackoverflow.com/questions/1089662/python-inflate-and-deflate-implementations
else:
data = gzip.GzipFile("", "rb", 9, io.BytesIO(page))
size = struct.unpack("<l", page[-4:])[0] # Reference: http://pydoc.org/get.cgi/usr/local/lib/python2.5/gzip.py
if size > MAX_CONNECTION_TOTAL_SIZE:
raise Exception("size too large")
page = data.read()
except Exception as ex:
if b"<html" not in page: # in some cases, invalid "Content-Encoding" appears for plain HTML (should be ignored)
errMsg = "detected invalid data for declared content "
errMsg += "encoding '%s' ('%s')" % (contentEncoding, getSafeExString(ex))
singleTimeLogMessage(errMsg, logging.ERROR)
warnMsg = "turning off page compression"
singleTimeWarnMessage(warnMsg)
kb.pageCompress = False
raise SqlmapCompressionException
if not conf.encoding:
httpCharset, metaCharset = None, None
# Reference: http://stackoverflow.com/questions/1020892/python-urllib2-read-to-unicode
if contentType.find("charset=") != -1:
httpCharset = checkCharEncoding(contentType.split("charset=")[-1])
metaCharset = checkCharEncoding(extractRegexResult(META_CHARSET_REGEX, page))
if (any((httpCharset, metaCharset)) and (not all((httpCharset, metaCharset)) or isinstance(page, six.binary_type) and all(_ in PRINTABLE_BYTES for _ in page))) or (httpCharset == metaCharset and all((httpCharset, metaCharset))):
kb.pageEncoding = httpCharset or metaCharset # Reference: http://bytes.com/topic/html-css/answers/154758-http-equiv-vs-true-header-has-precedence
debugMsg = "declared web page charset '%s'" % kb.pageEncoding
singleTimeLogMessage(debugMsg, logging.DEBUG, debugMsg)
else:
kb.pageEncoding = None
else:
kb.pageEncoding = conf.encoding
# can't do for all responses because we need to support binary files too
if isinstance(page, six.binary_type) and "text/" in contentType:
if not kb.disableHtmlDecoding:
# e.g. &#x9;&#195;&#235;&#224;&#226;&#224;
if b"&#" in page:
page = re.sub(b"&#x([0-9a-f]{1,2});", lambda _: decodeHex(_.group(1) if len(_.group(1)) == 2 else b"0%s" % _.group(1)), page)
page = re.sub(b"&#(\\d{1,3});", lambda _: six.int2byte(int(_.group(1))) if int(_.group(1)) < 256 else _.group(0), page)
# e.g. %20%28%29
if percentDecode:
if b"%" in page:
page = re.sub(b"%([0-9a-f]{2})", lambda _: decodeHex(_.group(1)), page)
page = re.sub(b"%([0-9A-F]{2})", lambda _: decodeHex(_.group(1)), page) # Note: %DeepSee_SQL in CACHE
# e.g. &amp;
page = re.sub(b"&([^;]+);", lambda _: six.int2byte(HTML_ENTITIES[getText(_.group(1))]) if HTML_ENTITIES.get(getText(_.group(1)), 256) < 256 else _.group(0), page)
kb.pageEncoding = kb.pageEncoding or checkCharEncoding(getHeuristicCharEncoding(page))
if (kb.pageEncoding or "").lower() == "utf-8-sig":
kb.pageEncoding = "utf-8"
if page and page.startswith(b"\xef\xbb\xbf"): # Reference: https://docs.python.org/2/library/codecs.html (Note: noticed problems when "utf-8-sig" is left to Python for handling)
page = page[3:]
page = getUnicode(page, kb.pageEncoding)
# e.g. &#8217;&#8230;&#8482;
if "&#" in page:
def _(match):
retVal = match.group(0)
try:
retVal = _unichr(int(match.group(1)))
except (ValueError, OverflowError):
pass
return retVal
page = re.sub(r"&#(\d+);", _, page)
# e.g. &zeta;
page = re.sub(r"&([^;]+);", lambda _: _unichr(HTML_ENTITIES[_.group(1)]) if HTML_ENTITIES.get(_.group(1), 0) > 255 else _.group(0), page)
else:
page = getUnicode(page, kb.pageEncoding)
return page
def processResponse(page, responseHeaders, code=None, status=None):
kb.processResponseCounter += 1
page = page or ""
parseResponse(page, responseHeaders if kb.processResponseCounter < PARSE_HEADERS_LIMIT else None, status)
if not kb.tableFrom and Backend.getIdentifiedDbms() in (DBMS.ACCESS,):
kb.tableFrom = extractRegexResult(SELECT_FROM_TABLE_REGEX, page)
else:
kb.tableFrom = None
if conf.parseErrors:
msg = extractErrorMessage(page)
if msg:
logger.warning("parsed DBMS error message: '%s'" % msg.rstrip('.'))
if not conf.skipWaf and kb.processResponseCounter < IDENTYWAF_PARSE_LIMIT:
rawResponse = "%s %s %s\n%s\n%s" % (_http_client.HTTPConnection._http_vsn_str, code or "", status or "", "".join(getUnicode(responseHeaders.headers if responseHeaders else [])), page[:HEURISTIC_PAGE_SIZE_THRESHOLD])
with kb.locks.identYwaf:
identYwaf.non_blind.clear()
if identYwaf.non_blind_check(rawResponse, silent=True):
for waf in set(identYwaf.non_blind):
if waf not in kb.identifiedWafs:
kb.identifiedWafs.add(waf)
errMsg = "WAF/IPS identified as '%s'" % identYwaf.format_name(waf)
singleTimeLogMessage(errMsg, logging.CRITICAL)
if kb.originalPage is None:
for regex in (EVENTVALIDATION_REGEX, VIEWSTATE_REGEX):
match = re.search(regex, page)
if match and PLACE.POST in conf.parameters:
name, value = match.groups()
if PLACE.POST in conf.paramDict and name in conf.paramDict[PLACE.POST]:
if conf.paramDict[PLACE.POST][name] in page:
continue
else:
msg = "do you want to automatically adjust the value of '%s'? [y/N]" % name
if not readInput(msg, default='N', boolean=True):
continue
conf.paramDict[PLACE.POST][name] = value
conf.parameters[PLACE.POST] = re.sub(r"(?i)(%s=)[^&]+" % re.escape(name), r"\g<1>%s" % value.replace('\\', r'\\'), conf.parameters[PLACE.POST])
if not kb.browserVerification and re.search(r"(?i)browser.?verification", page or ""):
kb.browserVerification = True
warnMsg = "potential browser verification protection mechanism detected"
if re.search(r"(?i)CloudFlare", page):
warnMsg += " (CloudFlare)"
singleTimeWarnMessage(warnMsg)
if not kb.captchaDetected and re.search(r"(?i)captcha", page or ""):
for match in re.finditer(r"(?si)<form.+?</form>", page):
if re.search(r"(?i)captcha", match.group(0)):
kb.captchaDetected = True
break
if re.search(r"<meta[^>]+\brefresh\b[^>]+\bcaptcha\b", page):
kb.captchaDetected = True
if kb.captchaDetected:
warnMsg = "potential CAPTCHA protection mechanism detected"
if re.search(r"(?i)<title>[^<]*CloudFlare", page):
warnMsg += " (CloudFlare)"
singleTimeWarnMessage(warnMsg)
if re.search(BLOCKED_IP_REGEX, page):
warnMsg = "it appears that you have been blocked by the target server"
singleTimeWarnMessage(warnMsg)

@ -5,34 +5,63 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入urllib库并重命名为_urllib
from thirdparty.six.moves import urllib as _urllib
class SmartHTTPBasicAuthHandler(_urllib.request.HTTPBasicAuthHandler):
"""
Reference: http://selenic.com/hg/rev/6c51a5056020
Fix for a: http://bugs.python.org/issue8797
参考: http://selenic.com/hg/rev/6c51a5056020
修复Bug: http://bugs.python.org/issue8797
这是一个处理HTTP基础认证的智能处理器类,继承自HTTPBasicAuthHandler
主要用于处理认证重试的逻辑,避免无限循环重试的问题
"""
def __init__(self, *args, **kwargs):
# 调用父类的初始化方法
_urllib.request.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
# 创建一个集合用于存储已重试过的请求
self.retried_req = set()
# 重试计数器初始化为0
self.retried_count = 0
def reset_retry_count(self):
# Python 2.6.5 will call this on 401 or 407 errors and thus loop
# forever. We disable reset_retry_count completely and reset in
# http_error_auth_reqed instead.
"""
重置重试计数的方法
Python 2.6.5在遇到401或407错误时会调用此方法,可能导致无限循环
因此这里禁用了重置功能,改为在http_error_auth_reqed中进行重置
"""
pass
def http_error_auth_reqed(self, auth_header, host, req, headers):
# Reset the retry counter once for each request.
"""
处理需要认证的HTTP错误
参数说明:
auth_header: 认证头信息
host: 目标主机
req: 请求对象
headers: 请求头
处理逻辑:
1. 对每个新请求重置重试计数器
2. 限制最大重试次数为5次
3. 超过重试次数则抛出HTTP 401错误
"""
# 如果是新的请求(通过hash判断)
if hash(req) not in self.retried_req:
# 将请求添加到已重试集合中
self.retried_req.add(hash(req))
# 重置重试计数
self.retried_count = 0
else:
# 如果重试次数超过5次
if self.retried_count > 5:
# 抛出HTTP 401认证失败错误
raise _urllib.error.HTTPError(req.get_full_url(), 401, "basic auth failed", headers, None)
else:
# 增加重试计数
self.retried_count += 1
# 调用父类的错误处理方法
return _urllib.request.HTTPBasicAuthHandler.http_error_auth_reqed(self, auth_header, host, req, headers)

@ -5,37 +5,63 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.data import conf
from lib.core.enums import HTTP_HEADER
from thirdparty.six.moves import urllib as _urllib
# 导入必要的模块
from lib.core.data import conf # 导入配置信息
from lib.core.enums import HTTP_HEADER # 导入HTTP头部常量定义
from thirdparty.six.moves import urllib as _urllib # 导入urllib模块并重命名为_urllib,用于处理URL和HTTP请求
class ChunkedHandler(_urllib.request.HTTPHandler):
"""
Ensures that HTTPHandler is working properly in case of Chunked Transfer-Encoding
用于确保在使用分块传输编码(Chunked Transfer-Encoding)时HTTPHandler能正常工作的处理器类
继承自urllib的HTTPHandler类,主要用于处理HTTP请求的发送
分块传输编码允许HTTP消息在不预先知道消息总长度的情况下进行传输
"""
def _http_request(self, request):
"""
处理HTTP请求的核心方法
参数:
request: HTTP请求对象,包含请求的所有信息(如URL头部数据等)
返回:
处理后的request对象
"""
# 获取请求的主机名,优先使用get_host()方法(新版本),如果不存在则使用host属性(旧版本)
host = request.get_host() if hasattr(request, "get_host") else request.host
if not host:
# 如果没有指定主机名则抛出异常,因为HTTP请求必须知道发送到哪个主机
raise _urllib.error.URLError("no host given")
if request.data is not None: # POST
if request.data is not None: # 如果是POST请求(包含数据)
data = request.data
# 如果没有设置Content-Type头,则设置为默认的表单格式
# application/x-www-form-urlencoded 是最常见的POST数据格式
if not request.has_header(HTTP_HEADER.CONTENT_TYPE):
request.add_unredirected_header(HTTP_HEADER.CONTENT_TYPE, "application/x-www-form-urlencoded")
# 如果没有设置Content-Length头且不使用分块传输,则设置内容长度
# Content-Length告诉服务器请求体的具体长度
if not request.has_header(HTTP_HEADER.CONTENT_LENGTH) and not conf.chunked:
request.add_unredirected_header(HTTP_HEADER.CONTENT_LENGTH, "%d" % len(data))
# 设置用于选择的主机名
sel_host = host
# 如果使用了代理,则从请求选择器中解析出实际的主机名
# 代理请求时,请求行中包含完整的URL,需要从中提取出真实的主机名
if request.has_proxy():
sel_host = _urllib.parse.urlsplit(request.get_selector()).netloc
# 如果没有设置Host头,则添加Host头
# Host头是HTTP/1.1必需的头部,用于指定服务器的域名和端口号
if not request.has_header(HTTP_HEADER.HOST):
request.add_unredirected_header(HTTP_HEADER.HOST, sel_host)
# 遍历父类中定义的额外头部信息
for name, value in self.parent.addheaders:
name = name.capitalize()
name = name.capitalize() # 将头部名称首字母大写,符合HTTP协议规范
# 如果请求中没有该头部,则添加到请求中
# 这确保了自定义头部不会覆盖已有的头部
if not request.has_header(name):
request.add_unredirected_header(name, value)
return request
# 将_http_request方法赋值给http_request,使其成为标准的处理方法
# 这是一种Python的惯用法,允许在保留原始方法的同时提供一个公开的接口
http_request = _http_request

@ -5,10 +5,12 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入未来版本的除法运算
from __future__ import division
import re
# 导入所需的工具函数
from lib.core.common import extractRegexResult
from lib.core.common import getFilteredPageContent
from lib.core.common import listToStrValue
@ -35,15 +37,28 @@ from lib.core.threads import getCurrentThreadData
from thirdparty import six
def comparison(page, headers, code=None, getRatioValue=False, pageLength=None):
"""
页面比较的主入口函数
:param page: 要比较的页面内容
:param headers: HTTP响应头
:param code: HTTP状态码
:param getRatioValue: 是否返回相似度值
:param pageLength: 页面长度
:return: 比较结果
"""
_ = _adjust(_comparison(page, headers, code, getRatioValue, pageLength), getRatioValue)
return _
def _adjust(condition, getRatioValue):
"""
调整比较结果
:param condition: 原始比较结果
:param getRatioValue: 是否返回相似度值
:return: 调整后的结果
"""
if not any((conf.string, conf.notString, conf.regexp, conf.code)):
# Negative logic approach is used in raw page comparison scheme as that what is "different" than original
# PAYLOAD.WHERE.NEGATIVE response is considered as True; in switch based approach negative logic is not
# applied as that what is by user considered as True is that what is returned by the comparison mechanism
# itself
# 在原始页面比较方案中使用负逻辑方法
# 与原始PAYLOAD.WHERE.NEGATIVE响应"不同"的内容被认为是True
retVal = not condition if kb.negativeLogic and condition is not None and not getRatioValue else condition
else:
retVal = condition if not getRatioValue else (MAX_RATIO if condition else MIN_RATIO)
@ -51,24 +66,37 @@ def _adjust(condition, getRatioValue):
return retVal
def _comparison(page, headers, code, getRatioValue, pageLength):
"""
核心比较函数
:param page: 要比较的页面内容
:param headers: HTTP响应头
:param code: HTTP状态码
:param getRatioValue: 是否返回相似度值
:param pageLength: 页面长度
:return: 比较结果
"""
# 获取当前线程数据
threadData = getCurrentThreadData()
# 测试模式下保存比较数据
if kb.testMode:
threadData.lastComparisonHeaders = listToStrValue(_ for _ in headers.headers if not _.startswith("%s:" % URI_HTTP_HEADER)) if headers else ""
threadData.lastComparisonPage = page
threadData.lastComparisonCode = code
# 页面内容和长度都为空时返回None
if page is None and pageLength is None:
return None
# 处理字符串匹配情况
if any((conf.string, conf.notString, conf.regexp)):
rawResponse = "%s%s" % (listToStrValue(_ for _ in headers.headers if not _.startswith("%s:" % URI_HTTP_HEADER)) if headers else "", page)
# String to match in page when the query is True
# 检查页面是否包含指定的匹配字符串
if conf.string:
return conf.string in rawResponse
# String to match in page when the query is False
# 检查页面是否不包含指定的字符串
if conf.notString:
if conf.notString in rawResponse:
return False
@ -78,24 +106,25 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
else:
return True
# Regular expression to match in page when the query is True and/or valid
# 使用正则表达式匹配页面内容
if conf.regexp:
return re.search(conf.regexp, rawResponse, re.I | re.M) is not None
# HTTP code to match when the query is valid
# 检查HTTP状态码是否匹配
if conf.code:
return conf.code == code
# 初始化序列匹配器
seqMatcher = threadData.seqMatcher
seqMatcher.set_seq1(kb.pageTemplate)
if page:
# In case of an DBMS error page return None
# 处理数据库错误页面
if kb.errorIsNone and (wasLastResponseDBMSError() or wasLastResponseHTTPError()) and not kb.negativeLogic:
if not (wasLastResponseHTTPError() and getLastRequestHTTPError() in (conf.ignoreCode or [])):
return None
# Dynamic content lines to be excluded before comparison
# 移除动态内容后再比较
if not kb.nullConnection:
page = removeDynamicContent(page)
seqMatcher.set_seq1(removeDynamicContent(kb.pageTemplate))
@ -103,6 +132,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
if not pageLength:
pageLength = len(page)
# 处理空连接情况
if kb.nullConnection and pageLength:
if not seqMatcher.a:
errMsg = "problem occurred while retrieving original page content "
@ -110,18 +140,19 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
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
else:
# Preventing "Unicode equal comparison failed to convert both arguments to Unicode"
# (e.g. if one page is PDF and the other is HTML)
# 处理编码问题,确保可以正确比较
if isinstance(seqMatcher.a, six.binary_type) and isinstance(page, six.text_type):
page = getBytes(page, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore")
elif isinstance(seqMatcher.a, six.text_type) and isinstance(page, six.binary_type):
seqMatcher.a = getBytes(seqMatcher.a, kb.pageEncoding or DEFAULT_PAGE_ENCODING, "ignore")
# 处理各种比较情况
if any(_ is None for _ in (page, seqMatcher.a)):
return None
elif seqMatcher.a and page and seqMatcher.a == page:
@ -136,6 +167,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
else:
seq1, seq2 = None, None
# 根据配置选择比较内容
if conf.titles:
seq1 = extractRegexResult(HTML_TITLE_REGEX, seqMatcher.a)
seq2 = extractRegexResult(HTML_TITLE_REGEX, page)
@ -146,13 +178,14 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
if seq1 is None or seq2 is None:
return None
# 移除反射值标记
seq1 = seq1.replace(REFLECTED_VALUE_MARKER, "")
seq2 = seq2.replace(REFLECTED_VALUE_MARKER, "")
# 处理高度动态内容
if kb.heavilyDynamic:
seq1 = seq1.split("\n")
seq2 = seq2.split("\n")
key = None
else:
key = (hash(seq1), hash(seq2))
@ -160,6 +193,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
seqMatcher.set_seq1(seq1)
seqMatcher.set_seq2(seq2)
# 使用缓存提高性能
if key in kb.cache.comparison:
ratio = kb.cache.comparison[key]
else:
@ -168,8 +202,7 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
if key:
kb.cache.comparison[key] = ratio
# If the url is stable and we did not set yet the match ratio and the
# current injected value changes the url page content
# 设置匹配比率
if kb.matchRatio is None:
if ratio >= LOWER_RATIO_BOUND and ratio <= UPPER_RATIO_BOUND:
kb.matchRatio = ratio
@ -178,19 +211,14 @@ def _comparison(page, headers, code, getRatioValue, pageLength):
if kb.testMode:
threadData.lastComparisonRatio = ratio
# If it has been requested to return the ratio and not a comparison
# response
# 根据不同情况返回结果
if getRatioValue:
return ratio
elif ratio > UPPER_RATIO_BOUND:
return True
elif ratio < LOWER_RATIO_BOUND:
return False
elif kb.matchRatio is None:
return None
else:
return (ratio - kb.matchRatio) > DIFF_TOLERANCE

@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入所需的标准库
import binascii
import inspect
import logging
@ -18,6 +19,7 @@ import sys
import time
import traceback
# 尝试导入websocket库,如果不存在则定义一个简单的异常类
try:
import websocket
from websocket import WebSocketException
@ -25,6 +27,7 @@ except ImportError:
class WebSocketException(Exception):
pass
# 导入sqlmap自定义的库和工具函数
from lib.core.agent import agent
from lib.core.common import asciifyUrl
from lib.core.common import calculateDeltaSeconds
@ -146,13 +149,18 @@ from thirdparty.socks.socks import ProxyError
class Connect(object):
"""
This class defines methods used to perform HTTP requests
这个类定义了用于执行HTTP请求的方法
"""
@staticmethod
def _getPageProxy(**kwargs):
"""
代理方法,用于处理页面请求
检查递归深度并调用getPage方法
"""
try:
if (len(inspect.stack()) > sys.getrecursionlimit() // 2): # Note: https://github.com/sqlmapproject/sqlmap/issues/4525
# 检查调用栈深度是否超过限制
if (len(inspect.stack()) > sys.getrecursionlimit() // 2):
warnMsg = "unable to connect to the target URL"
raise SqlmapConnectionException(warnMsg)
except (TypeError, UnicodeError):
@ -165,9 +173,15 @@ class Connect(object):
@staticmethod
def _retryProxy(**kwargs):
"""
重试代理方法
处理请求失败时的重试逻辑
"""
# 获取当前线程数据
threadData = getCurrentThreadData()
threadData.retriesCount += 1
# 如果配置了代理列表且重试次数达到上限,则更换代理
if conf.proxyList and threadData.retriesCount >= conf.retries and not kb.locks.handlers.locked():
warnMsg = "changing proxy"
logger.warning(warnMsg)
@ -177,9 +191,8 @@ class Connect(object):
setHTTPHandlers()
# 处理基于时间的测试模式
if kb.testMode and kb.previousMethod == PAYLOAD.METHOD.TIME:
# timed based payloads can cause web server unresponsiveness
# if the injectable piece of code is some kind of JOIN-like query
warnMsg = "most likely web server instance hasn't recovered yet "
warnMsg += "from previous timed based payload. If the problem "
warnMsg += "persists please wait for a few minutes and rerun "
@ -188,6 +201,7 @@ class Connect(object):
warnMsg += "lower the value of option '--time-sec' (e.g. '--time-sec=2')"
singleTimeWarnMessage(warnMsg)
# 处理原始页面为空的情况
elif kb.originalPage is None:
if conf.tor:
warnMsg = "please make sure that you have "
@ -214,20 +228,28 @@ class Connect(object):
singleTimeWarnMessage(warnMsg)
# 处理多线程情况
elif conf.threads > 1:
warnMsg = "if the problem persists please try to lower "
warnMsg += "the number of used threads (option '--threads')"
singleTimeWarnMessage(warnMsg)
# 重试请求
kwargs['retrying'] = True
return Connect._getPageProxy(**kwargs)
@staticmethod
def _connReadProxy(conn):
"""
读取连接响应的代理方法
处理压缩和大响应的情况
"""
retVal = b""
# 如果不是DNS模式且连接存在
if not kb.dnsMode and conn:
headers = conn.info()
# 处理压缩响应
if kb.pageCompress and headers and hasattr(headers, "getheader") and (headers.getheader(HTTP_HEADER.CONTENT_ENCODING, "").lower() in ("gzip", "deflate") or "text" not in headers.getheader(HTTP_HEADER.CONTENT_TYPE, "").lower()):
retVal = conn.read(MAX_CONNECTION_TOTAL_SIZE)
if len(retVal) == MAX_CONNECTION_TOTAL_SIZE:
@ -236,6 +258,7 @@ class Connect(object):
kb.pageCompress = False
raise SqlmapCompressionException
else:
# 分块读取大响应
while True:
if not conn:
break
@ -254,11 +277,13 @@ class Connect(object):
retVal += part
break
# 检查总响应大小是否超过限制
if len(retVal) > MAX_CONNECTION_TOTAL_SIZE:
warnMsg = "too large response detected. Automatically trimming it"
singleTimeWarnMessage(warnMsg)
break
# 处理特殊的响应放大因子
if conf.yuge:
retVal = YUGE_FACTOR * retVal
@ -267,13 +292,14 @@ class Connect(object):
@staticmethod
def getPage(**kwargs):
"""
This method connects to the target URL or proxy and returns
the target URL page content
这个方法连接到目标URL或代理并返回目标URL页面内容
"""
# 如果是离线模式直接返回
if conf.offline:
return None, None, None
# 获取请求参数
url = kwargs.get("url", None) or conf.url
get = kwargs.get("get", None)
post = kwargs.get("post", None)
@ -297,16 +323,19 @@ class Connect(object):
finalCode = kwargs.get("finalCode", False)
chunked = kwargs.get("chunked", False) or conf.chunked
# 处理请求延迟
if isinstance(conf.delay, (int, float)) and conf.delay > 0:
time.sleep(conf.delay)
start = time.time()
# 获取当前线程数据
threadData = getCurrentThreadData()
with kb.locks.request:
kb.requestCounter += 1
threadData.lastRequestUID = kb.requestCounter
# 处理代理频率
if conf.proxyFreq:
if kb.requestCounter % conf.proxyFreq == 0:
conf.proxy = None
@ -316,6 +345,7 @@ class Connect(object):
setHTTPHandlers()
# 处理测试模式
if conf.dummy or conf.murphyRate and randomInt() % conf.murphyRate == 0:
if conf.murphyRate:
time.sleep(randomInt() % (MAX_MURPHY_SLEEP_TIME + 1))
@ -327,6 +357,7 @@ class Connect(object):
return page, headers, code
# 处理cookie
if conf.liveCookies:
with kb.locks.liveCookies:
if not checkFile(conf.liveCookies, raiseOnError=False) or os.path.getsize(conf.liveCookies) == 0:
@ -351,6 +382,7 @@ class Connect(object):
cookie = openFile(conf.liveCookies).read().strip()
cookie = re.sub(r"(?i)\ACookie:\s*", "", cookie)
# 处理multipart请求
if multipart:
post = multipart
else:
@ -361,20 +393,20 @@ class Connect(object):
post = _urllib.parse.unquote(post)
post = chunkSplitPostData(post)
# 处理WebSocket请求
webSocket = url.lower().startswith("ws")
if not _urllib.parse.urlsplit(url).netloc:
url = _urllib.parse.urljoin(conf.url, url)
# flag to know if we are dealing with the same target host
# 检查是否是相同的目标主机
target = checkSameHost(url, conf.url)
if not retrying:
# Reset the number of connection retries
# 重置连接重试次数
threadData.retriesCount = 0
# fix for known issue when urllib2 just skips the other part of provided
# url splitted with space char while urlencoding it in the later phase
# 修复URL中的空格
url = url.replace(" ", "%20")
if "://" not in url:
@ -396,8 +428,7 @@ class Connect(object):
raise404 = raise404 and not kb.ignoreNotFound
# support for non-latin (e.g. cyrillic) URLs as urllib/urllib2 doesn't
# support those by default
# 支持非拉丁字符的URL
url = asciifyUrl(url)
try:
@ -440,7 +471,7 @@ class Connect(object):
requestMsg += " %s" % _http_client.HTTPConnection._http_vsn_str
# Prepare HTTP headers
# 准备HTTP头
headers = forgeHeaders({HTTP_HEADER.COOKIE: cookie, HTTP_HEADER.USER_AGENT: ua, HTTP_HEADER.REFERER: referer, HTTP_HEADER.HOST: getHeader(dict(conf.httpHeaders), HTTP_HEADER.HOST) or getHostHeader(url)}, base=None if target else {})
if HTTP_HEADER.COOKIE in headers:
@ -624,11 +655,11 @@ class Connect(object):
if not kb.proxyAuthHeader and getRequestHeader(req, HTTP_HEADER.PROXY_AUTHORIZATION):
kb.proxyAuthHeader = getRequestHeader(req, HTTP_HEADER.PROXY_AUTHORIZATION)
# Return response object
# 返回响应对象
if response:
return conn, None, None
# Get HTTP response
# 获取HTTP响应
if hasattr(conn, "redurl"):
page = (threadData.lastRedirectMsg[1] if kb.choices.redirect == REDIRECTION.NO else Connect._connReadProxy(conn)) if not skipRead else None
skipLogTraffic = kb.choices.redirect == REDIRECTION.NO

@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入所需的模块
import re
import time
@ -30,58 +31,87 @@ from lib.utils.safe2bin import safecharencode
from lib.utils.timeout import timeout
def direct(query, content=True):
"""
直接执行SQL查询的主函数
参数:
query: 要执行的SQL查询语句
content: 是否返回查询内容,默认为True
"""
# 标记是否为SELECT查询
select = True
# 处理查询语句,添加必要的payload
query = agent.payloadDirect(query)
query = agent.adjustLateValues(query)
# 获取当前线程数据
threadData = getCurrentThreadData()
# 针对Oracle数据库的特殊处理:如果是不带FROM的SELECT语句,添加"FROM DUAL"
if Backend.isDbms(DBMS.ORACLE) and query.upper().startswith("SELECT ") and " FROM " not in query.upper():
query = "%s FROM DUAL" % query
# 通过遍历SQL语句字典判断是否为SELECT查询
for sqlTitle, sqlStatements in SQL_STATEMENTS.items():
for sqlStatement in sqlStatements:
if query.lower().startswith(sqlStatement) and sqlTitle != "SQL SELECT statement":
select = False
break
# 如果是SELECT查询,进行相应处理
if select:
# 如果查询不以SELECT开头,添加SELECT
if re.search(r"(?i)\ASELECT ", query) is None:
query = "SELECT %s" % query
# 处理二进制字段
if conf.binaryFields:
for field in conf.binaryFields:
field = field.strip()
if re.search(r"\b%s\b" % re.escape(field), query):
query = re.sub(r"\b%s\b" % re.escape(field), agent.hexConvertField(field), query)
# 记录查询语句到日志
logger.log(CUSTOM_LOGGING.PAYLOAD, query)
# 尝试从缓存中获取查询结果
output = hashDBRetrieve(query, True, True)
start = time.time()
# 执行查询
if not select and re.search(r"(?i)\bEXEC ", query) is None:
# 非SELECT且非EXEC语句的执行
timeout(func=conf.dbmsConnector.execute, args=(query,), duration=conf.timeout, default=None)
elif not (output and ("%soutput" % conf.tablePrefix) not in query and ("%sfile" % conf.tablePrefix) not in query):
# SELECT查询的执行
output, state = timeout(func=conf.dbmsConnector.select, args=(query,), duration=conf.timeout, default=None)
if state == TIMEOUT_STATE.NORMAL:
# 正常执行完成,将结果写入缓存
hashDBWrite(query, output, True)
elif state == TIMEOUT_STATE.TIMEOUT:
# 超时处理:关闭连接并重新连接
conf.dbmsConnector.close()
conf.dbmsConnector.connect()
elif output:
# 如果有缓存结果,显示提示信息
infoMsg = "resumed: %s..." % getUnicode(output, UNICODE_ENCODING)[:20]
logger.info(infoMsg)
# 记录查询执行时间
threadData.lastQueryDuration = calculateDeltaSeconds(start)
# 处理返回结果
if not output:
return output
elif content:
# 如果需要返回内容
if output and isListLike(output):
if len(output[0]) == 1:
# 如果结果只有一列,简化输出格式
output = [_[0] for _ in output]
# 转换为Unicode格式
retVal = getUnicode(output, noneToNull=True)
# 根据配置决定是否进行安全字符编码
return safecharencode(retVal) if kb.safeCharEncode else retVal
else:
# 如果不需要返回内容,提取预期的布尔值
return extractExpectedValue(output, EXPECTED.BOOL)

@ -7,16 +7,17 @@ See the file 'LICENSE' for copying permission
from __future__ import print_function
import binascii
import binascii # 用于二进制和ASCII转换
import os
import re
import socket
import struct
import threading
import socket # 用于网络通信
import struct # 用于处理字节串
import threading # 用于多线程
import time
class DNSQuery(object):
"""
DNS查询解析类
>>> DNSQuery(b'|K\\x01 \\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x01\\x03www\\x06google\\x03com\\x00\\x00\\x01\\x00\\x01\\x00\\x00)\\x10\\x00\\x00\\x00\\x00\\x00\\x00\\x0c\\x00\\n\\x00\\x08O4|Np!\\x1d\\xb3')._query == b"www.google.com."
True
>>> DNSQuery(b'\\x00')._query == b""
@ -24,12 +25,15 @@ class DNSQuery(object):
"""
def __init__(self, raw):
self._raw = raw
self._query = b""
self._raw = raw # 原始DNS查询数据
self._query = b"" # 解析后的域名查询字符串
try:
type_ = (ord(raw[2:3]) >> 3) & 15 # Opcode bits
# 从DNS报文中提取操作码(Opcode)
type_ = (ord(raw[2:3]) >> 3) & 15
if type_ == 0: # 标准查询
i = 12 # DNS报文头部长度为12字节
if type_ == 0: # Standard query
i = 12
j = ord(raw[i:i + 1])

@ -5,6 +5,7 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入所需的标准库和第三方库
import re
import socket
@ -20,6 +21,7 @@ from lib.core.settings import PYVERSION
from thirdparty.six.moves import http_client as _http_client
from thirdparty.six.moves import urllib as _urllib
# 尝试导入ssl模块,如果导入失败则ssl为None
ssl = None
try:
import ssl as _ssl
@ -27,31 +29,38 @@ try:
except ImportError:
pass
# 获取所有可用的SSL/TLS协议版本,并存储在_protocols列表中
_protocols = filterNone(getattr(ssl, _, None) for _ in ("PROTOCOL_TLS_CLIENT", "PROTOCOL_TLSv1_2", "PROTOCOL_TLSv1_1", "PROTOCOL_TLSv1", "PROTOCOL_SSLv3", "PROTOCOL_SSLv23", "PROTOCOL_SSLv2"))
# 创建协议名称和值的映射字典
_lut = dict((getattr(ssl, _), _) for _ in dir(ssl) if _.startswith("PROTOCOL_"))
# 存储SSL上下文的字典
_contexts = {}
class HTTPSConnection(_http_client.HTTPSConnection):
"""
Connection class that enables usage of newer SSL protocols.
自定义HTTPS连接类,支持使用较新的SSL协议
继承自标准库的HTTPSConnection类
Reference: http://bugs.python.org/msg128686
参考: http://bugs.python.org/msg128686
NOTE: use https://check-tls.akamaized.net/ to check if (e.g.) TLS/SNI is working properly
注意: 使用 https://check-tls.akamaized.net/ 检查TLS/SNI是否正常工作
"""
def __init__(self, *args, **kwargs):
# NOTE: Dirty patch for https://bugs.python.org/issue38251 / https://github.com/sqlmapproject/sqlmap/issues/4158
# 修复Python bug: https://bugs.python.org/issue38251
if hasattr(ssl, "_create_default_https_context"):
if None not in _contexts:
_contexts[None] = ssl._create_default_https_context()
kwargs["context"] = _contexts[None]
# 重试标志
self.retrying = False
# 调用父类初始化方法
_http_client.HTTPSConnection.__init__(self, *args, **kwargs)
def connect(self):
# 创建socket连接的内部函数
def create_sock():
sock = socket.create_connection((self.host, self.port), self.timeout)
if getattr(self, "_tunnel_host", None):
@ -61,31 +70,33 @@ class HTTPSConnection(_http_client.HTTPSConnection):
success = False
# Reference(s): https://docs.python.org/2/library/ssl.html#ssl.SSLContext
# https://www.mnot.net/blog/2014/12/27/python_2_and_tls_sni
# 使用SSLContext方式建立SSL连接(Python 2.7.9及更高版本支持)
if hasattr(ssl, "SSLContext"):
for protocol in (_ for _ in _protocols if _ >= ssl.PROTOCOL_TLSv1):
try:
sock = create_sock()
if protocol not in _contexts:
# 创建SSL上下文
_contexts[protocol] = ssl.SSLContext(protocol)
# Disable certificate and hostname validation enabled by default with PROTOCOL_TLS_CLIENT
# 禁用证书和主机名验证
_contexts[protocol].check_hostname = False
_contexts[protocol].verify_mode = ssl.CERT_NONE
# 如果提供了证书和密钥文件,则加载它们
if getattr(self, "cert_file", None) and getattr(self, "key_file", None):
_contexts[protocol].load_cert_chain(certfile=self.cert_file, keyfile=self.key_file)
try:
# Reference(s): https://askubuntu.com/a/1263098
# https://askubuntu.com/a/1250807
# 设置加密套件
_contexts[protocol].set_ciphers("DEFAULT@SECLEVEL=1")
except (ssl.SSLError, AttributeError):
pass
# 包装socket并进行SSL握手
result = _contexts[protocol].wrap_socket(sock, do_handshake_on_connect=True, server_hostname=self.host if re.search(r"\A[\d.]+\Z", self.host or "") is None else None)
if result:
success = True
self.sock = result
# 将成功的协议移到列表开头
_protocols.remove(protocol)
_protocols.insert(0, protocol)
break
@ -95,6 +106,7 @@ class HTTPSConnection(_http_client.HTTPSConnection):
self._tunnel_host = None
logger.debug("SSL connection error occurred for '%s' ('%s')" % (_lut[protocol], getSafeExString(ex)))
# 使用旧式ssl.wrap_socket方式建立SSL连接(用于较老版本的Python)
elif hasattr(ssl, "wrap_socket"):
for protocol in _protocols:
try:
@ -112,12 +124,14 @@ class HTTPSConnection(_http_client.HTTPSConnection):
self._tunnel_host = None
logger.debug("SSL connection error occurred for '%s' ('%s')" % (_lut[protocol], getSafeExString(ex)))
# 如果所有协议都连接失败
if not success:
errMsg = "can't establish SSL connection"
# Reference: https://docs.python.org/2/library/ssl.html
# 对于Python 2.7.9之前的版本,建议升级
if LooseVersion(PYVERSION) < LooseVersion("2.7.9"):
errMsg += " (please retry with Python >= 2.7.9)"
# 如果之前有成功连接过且未在重试,则进行重试
if kb.sslSuccess and not self.retrying:
self.retrying = True
@ -134,5 +148,9 @@ class HTTPSConnection(_http_client.HTTPSConnection):
kb.sslSuccess = True
class HTTPSHandler(_urllib.request.HTTPSHandler):
"""
HTTPS处理器类,用于处理HTTPS请求
"""
def https_open(self, req):
# 根据是否有ssl模块选择合适的连接类来处理请求
return self.do_open(HTTPSConnection if ssl else _http_client.HTTPSConnection, req)

@ -5,16 +5,27 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入getText函数,用于文本转换
from lib.core.convert import getText
# 导入urllib库并重命名为_urllib,用于处理URL请求
from thirdparty.six.moves import urllib as _urllib
class MethodRequest(_urllib.request.Request):
"""
Used to create HEAD/PUT/DELETE/... requests with urllib
继承自urllib.request.Request类
用于创建HTTP请求(HEAD/PUT/DELETE等)的自定义请求类
"""
def set_method(self, method):
self.method = getText(method.upper()) # Dirty hack for Python3 (may it rot in hell!)
# 设置HTTP请求方法
# 参数method: 字符串类型,表示HTTP方法(如GET、POST、HEAD等)
# getText()将输入转换为文本格式
# upper()将文本转为大写
self.method = getText(method.upper()) # Python3兼容性处理
def get_method(self):
# 获取HTTP请求方法
# getattr用于获取对象的属性
# 如果self.method存在就返回它
# 否则返回父类Request默认的get_method()方法的结果
return getattr(self, 'method', _urllib.request.Request.get_method(self))

@ -5,25 +5,57 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.data import conf
from lib.core.common import getSafeExString
from lib.core.exception import SqlmapConnectionException
from thirdparty.six.moves import http_client as _http_client
from thirdparty.six.moves import urllib as _urllib
# 导入所需的模块
from lib.core.data import conf # 导入配置信息
from lib.core.common import getSafeExString # 导入安全字符串处理函数
from lib.core.exception import SqlmapConnectionException # 导入SQL注入连接异常类
from thirdparty.six.moves import http_client as _http_client # 导入HTTP客户端
from thirdparty.six.moves import urllib as _urllib # 导入URL处理库
class HTTPSPKIAuthHandler(_urllib.request.HTTPSHandler):
"""
HTTPS PKI认证处理器类
继承自urllib的HTTPSHandler类,用于处理带客户端证书的HTTPS请求
"""
def __init__(self, auth_file):
_urllib.request.HTTPSHandler.__init__(self)
self.auth_file = auth_file
"""
初始化函数
参数:
auth_file: 认证文件路径,包含客户端证书和私钥
"""
_urllib.request.HTTPSHandler.__init__(self) # 调用父类初始化
self.auth_file = auth_file # 保存认证文件路径
def https_open(self, req):
"""
处理HTTPS请求的方法
参数:
req: HTTPS请求对象
返回:
处理后的HTTPS连接
"""
return self.do_open(self.getConnection, req)
def getConnection(self, host, timeout=None):
"""
建立HTTPS连接的方法
参数:
host: 目标主机
timeout: 超时时间,默认为None
返回:
HTTPS连接对象
异常:
SqlmapConnectionException: 连接异常
"""
try:
# Reference: https://docs.python.org/2/library/ssl.html#ssl.SSLContext.load_cert_chain
return _http_client.HTTPSConnection(host, cert_file=self.auth_file, key_file=self.auth_file, timeout=conf.timeout)
# 创建带客户端证书的HTTPS连接
# 参考文档: https://docs.python.org/2/library/ssl.html#ssl.SSLContext.load_cert_chain
return _http_client.HTTPSConnection(host,
cert_file=self.auth_file, # 证书文件
key_file=self.auth_file, # 私钥文件
timeout=conf.timeout) # 超时设置
except IOError as ex:
# 如果出现IO错误(比如证书文件无法读取等)
errMsg = "error occurred while using key "
errMsg += "file '%s' ('%s')" % (self.auth_file, getSafeExString(ex))
raise SqlmapConnectionException(errMsg)
raise SqlmapConnectionException(errMsg) # 抛出SQL注入连接异常

@ -5,25 +5,44 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入自定义的SQL映射连接异常类
from lib.core.exception import SqlmapConnectionException
# 导入urllib库并重命名为_urllib,用于处理HTTP请求
from thirdparty.six.moves import urllib as _urllib
class HTTPRangeHandler(_urllib.request.BaseHandler):
"""
Handler that enables HTTP Range headers.
Reference: http://stackoverflow.com/questions/1971240/python-seek-on-remote-file
处理HTTP Range头部的处理器类
Range头部允许客户端请求资源的部分内容而不是整个资源
参考文档: http://stackoverflow.com/questions/1971240/python-seek-on-remote-file
"""
def http_error_206(self, req, fp, code, msg, hdrs):
# 206 Partial Content Response
"""
处理206状态码(部分内容响应)
参数说明:
req: 原始请求对象
fp: 类文件对象,包含响应内容
code: HTTP状态码(206)
msg: 响应消息
hdrs: 响应头部信息
"""
# 创建并返回一个包含部分内容的响应对象
r = _urllib.response.addinfourl(fp, hdrs, req.get_full_url())
r.code = code
r.msg = msg
r.code = code # 设置响应状态码
r.msg = msg # 设置响应消息
return r
def http_error_416(self, req, fp, code, msg, hdrs):
# HTTP's Range Not Satisfiable error
"""
处理416状态码(请求范围不满足)
当请求的范围超出资源的实际范围时会触发此错误
参数说明与http_error_206相同
"""
# 构建错误信息
errMsg = "there was a problem while connecting "
errMsg += "target ('406 - Range Not Satisfiable')"
# 抛出SQL映射连接异常
raise SqlmapConnectionException(errMsg)

@ -5,11 +5,13 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入所需的标准库和第三方库
import io
import re
import time
import types
# 导入自定义工具函数
from lib.core.common import getHostHeader
from lib.core.common import getSafeExString
from lib.core.common import logHTTPTraffic
@ -36,7 +38,17 @@ from thirdparty import six
from thirdparty.six.moves import urllib as _urllib
class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
"""
智能重定向处理器类,继承自urllib的HTTPRedirectHandler
用于处理HTTP重定向响应(301/302/303/307)
"""
def _get_header_redirect(self, headers):
"""
从响应头中获取重定向URL
:param headers: HTTP响应头
:return: 重定向URL或None
"""
retVal = None
if headers:
@ -48,13 +60,21 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
return retVal
def _ask_redirect_choice(self, redcode, redurl, method):
"""
询问用户是否要跟随重定向
:param redcode: 重定向状态码
:param redurl: 重定向URL
:param method: HTTP请求方法
"""
with kb.locks.redirect:
# 如果还没有做出重定向选择
if kb.choices.redirect is None:
msg = "got a %d redirect to " % redcode
msg += "'%s'. Do you want to follow? [Y/n] " % redurl
kb.choices.redirect = REDIRECTION.YES if readInput(msg, default='Y', boolean=True) else REDIRECTION.NO
# 如果是POST请求导致的重定向,询问是否要重发POST数据
if kb.choices.redirect == REDIRECTION.YES and method == HTTPMETHOD.POST and kb.resendPostOnRedirect is None:
msg = "redirect is a result of a "
msg += "POST request. Do you want to "
@ -67,31 +87,49 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
self.redirect_request = self._redirect_request
def _redirect_request(self, req, fp, code, msg, headers, newurl):
"""
创建重定向请求
:return: 新的Request对象
"""
return _urllib.request.Request(newurl.replace(' ', '%20'), data=req.data, headers=req.headers, origin_req_host=req.get_origin_req_host() if hasattr(req, "get_origin_req_host") else req.origin_req_host)
def http_error_302(self, req, fp, code, msg, headers):
"""
处理302重定向响应
:param req: 原始请求
:param fp: 响应文件对象
:param code: 状态码
:param msg: 状态消息
:param headers: 响应头
:return: 处理后的响应对象
"""
start = time.time()
content = None
forceRedirect = False
# 如果未忽略重定向,则获取重定向URL
redurl = self._get_header_redirect(headers) if not conf.ignoreRedirects else None
# 读取响应内容
try:
content = fp.read(MAX_CONNECTION_TOTAL_SIZE)
except: # e.g. IncompleteRead
except: # 处理不完整读取
content = b""
finally:
if content:
try: # try to write it back to the read buffer so we could reuse it in further steps
try: # 尝试将内容写回读取缓冲区以便后续使用
fp.fp._rbuf.truncate(0)
fp.fp._rbuf.write(content)
except:
pass
# 解码响应内容
content = decodePage(content, headers.get(HTTP_HEADER.CONTENT_ENCODING), headers.get(HTTP_HEADER.CONTENT_TYPE))
# 记录重定向信息
threadData = getCurrentThreadData()
threadData.lastRedirectMsg = (threadData.lastRequestUID, content)
# 构建重定向日志消息
redirectMsg = "HTTP redirect "
redirectMsg += "[#%d] (%d %s):\r\n" % (threadData.lastRequestUID, code, getUnicode(msg))
@ -104,31 +142,39 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
if content:
redirectMsg += "\r\n\r\n%s" % getUnicode(content[:MAX_CONNECTION_READ_SIZE])
# 记录HTTP流量日志
logHTTPTraffic(threadData.lastRequestMsg, redirectMsg, start, time.time())
logger.log(CUSTOM_LOGGING.TRAFFIC_IN, redirectMsg)
if redurl:
try:
# 如果重定向URL是相对路径,转换为绝对路径
if not _urllib.parse.urlsplit(redurl).netloc:
redurl = _urllib.parse.urljoin(req.get_full_url(), redurl)
# 检查是否存在无限重定向循环
self._infinite_loop_check(req)
if conf.scope:
# 检查重定向URL是否在指定范围内
if not re.search(conf.scope, redurl, re.I):
redurl = None
else:
forceRedirect = True
else:
# 询问用户是否跟随重定向
self._ask_redirect_choice(code, redurl, req.get_method())
except ValueError:
redurl = None
result = fp
if redurl and (kb.choices.redirect == REDIRECTION.YES or forceRedirect):
# 解析响应
parseResponse(content, headers)
# 更新请求头中的Host
req.headers[HTTP_HEADER.HOST] = getHostHeader(redurl)
if headers and HTTP_HEADER.SET_COOKIE in headers:
# 处理Cookie
cookies = dict()
delimiter = conf.cookieDel or DEFAULT_COOKIE_DELIMITER
last = None
@ -145,11 +191,12 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
req.headers[HTTP_HEADER.COOKIE] = delimiter.join("%s=%s" % (key, cookies[key]) for key in cookies)
try:
# 执行重定向请求
result = _urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
except _urllib.error.HTTPError as ex:
result = ex
# Dirty hack for https://github.com/sqlmapproject/sqlmap/issues/4046
# 处理特殊情况的hack
try:
hasattr(result, "read")
except KeyError:
@ -157,7 +204,6 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
pass
result = _()
# Dirty hack for http://bugs.python.org/issue15701
try:
result.info()
except AttributeError:
@ -169,7 +215,7 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
if not hasattr(result, "read"):
def _(self, length=None):
try:
retVal = getSafeExString(ex) # Note: pyflakes mistakenly marks 'ex' as undefined (NOTE: tested in both Python2 and Python3)
retVal = getSafeExString(ex)
except:
retVal = ""
return getBytes(retVal)
@ -188,15 +234,23 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
else:
result = fp
# 记录最后的重定向URL
threadData.lastRedirectURL = (threadData.lastRequestUID, redurl)
# 设置结果属性
result.redcode = code
result.redurl = getUnicode(redurl) if six.PY3 else redurl
return result
# 其他重定向状态码使用相同的处理方法
http_error_301 = http_error_303 = http_error_307 = http_error_302
def _infinite_loop_check(self, req):
"""
检查是否存在无限重定向循环
:param req: 请求对象
:raises: SqlmapConnectionException 如果检测到无限循环
"""
if hasattr(req, 'redirect_dict') and (req.redirect_dict.get(req.get_full_url(), 0) >= MAX_SINGLE_URL_REDIRECTIONS or len(req.redirect_dict) >= MAX_TOTAL_REDIRECTIONS):
errMsg = "infinite redirect loop detected (%s). " % ", ".join(item for item in req.redirect_dict.keys())
errMsg += "Please check all provided parameters and/or provide missing ones"

@ -5,17 +5,59 @@ Copyright (c) 2006-2024 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
# 导入必要的模块
# kb (Knowledge Base) 是一个全局对象,用于存储程序运行时的各种状态和数据
# 比如原始页面内容、错误状态、页面模板缓存等信息都存储在这里
from lib.core.data import kb
# 从connect模块导入Request类并重命名为Request
# Request类用于发送HTTP请求,是与目标网站交互的核心类
from lib.request.connect import Connect as Request
def getPageTemplate(payload, place):
"""
获取页面模板的函数
这个函数的主要作用是:
1. 根据给定的SQL注入payload和注入位置获取目标页面
2. 将页面内容缓存起来避免重复请求
3. 返回页面内容和错误状态信息
参数说明:
payload: SQL注入的载荷,即要注入的SQL代码
place: 注入点的位置,表示在请求中的哪个位置进行注入
(比如URL参数POST数据Cookie等)
返回值:
返回一个包含两个元素的元组:
1. 页面内容 - 可能是原始页面或注入后的页面
2. 错误状态 - 表示页面解析是否出现错误
"""
# 初始化返回值
# kb.originalPage 存储了未注入时的原始页面内容
# kb.errorIsNone 表示错误检查的状态
retVal = (kb.originalPage, kb.errorIsNone)
# 只有当payload和place都不为空时才执行注入操作
if payload and place:
# 检查这个payload和place的组合是否已经在缓存中
# kb.pageTemplates是一个字典,用于缓存不同注入组合的结果
if (payload, place) not in kb.pageTemplates:
# 如果没有缓存,则发送新的请求
# Request.queryPage方法用于发送带有注入payload的请求
# content=True 表示需要返回页面内容
# raise404=False 表示遇到404错误时不抛出异常
# 返回值中的page是页面内容,其他两个值用下划线忽略
page, _, _ = Request.queryPage(payload, place, content=True, raise404=False)
# 将结果存入缓存
# page 是获取到的页面内容
# kb.lastParserStatus is None 表示页面解析是否成功
# (None表示解析成功,非None表示解析出错)
kb.pageTemplates[(payload, place)] = (page, kb.lastParserStatus is None)
# 从缓存中获取之前存储的结果
retVal = kb.pageTemplates[(payload, place)]
# 返回页面内容和错误状态
return retVal

Loading…
Cancel
Save