mirror of
https://github.com/sqlmapproject/sqlmap.git
synced 2026-10-20 15:21:30 +08:00
Implements #4271
This commit is contained in:
@@ -463,6 +463,7 @@ class MKSTEMP_PREFIX(object):
|
||||
BIG_ARRAY = "sqlmapbigarray-"
|
||||
SPECIFIC_RESPONSE = "sqlmapresponse-"
|
||||
PREPROCESS = "sqlmappreprocess-"
|
||||
AUTH_REFRESH = "sqlmapauthrefresh-"
|
||||
|
||||
class TIMEOUT_STATE(object):
|
||||
NORMAL = 0
|
||||
|
||||
+44
-1
@@ -142,6 +142,7 @@ from lib.parse.configfile import configFileParser
|
||||
from lib.parse.payloads import loadBoundaries
|
||||
from lib.parse.payloads import loadPayloads
|
||||
from lib.request.basic import checkCharEncoding
|
||||
from lib.request import authrefresh
|
||||
from lib.request.basicauthhandler import SmartHTTPBasicAuthHandler
|
||||
from lib.request.chunkedhandler import ChunkedHandler
|
||||
from lib.request.connect import Connect as Request
|
||||
@@ -1152,6 +1153,42 @@ def _setPostprocessFunctions():
|
||||
errMsg += "(Note: find template script at '%s')" % filename
|
||||
raise SqlmapGenericException(errMsg)
|
||||
|
||||
def _setAuthRefreshFunction():
|
||||
"""
|
||||
Load the authentication refresh hook without performing a login during initialization.
|
||||
"""
|
||||
|
||||
if conf.authRefresh:
|
||||
try:
|
||||
script = safeFilepathEncode(conf.authRefresh.strip())
|
||||
|
||||
if not os.path.isfile(script):
|
||||
raise SqlmapFilePathException("auth refresh script '%s' does not exist" % script)
|
||||
if not script.endswith(".py"):
|
||||
raise SqlmapSyntaxException("auth refresh script should have an extension '.py'")
|
||||
|
||||
dirname, filename = os.path.split(os.path.abspath(script))
|
||||
if dirname not in sys.path:
|
||||
sys.path.insert(0, dirname)
|
||||
|
||||
logger.info("loading auth refresh module '%s'" % filename[:-3])
|
||||
try:
|
||||
getattr(importlib, "invalidate_caches", lambda: None)()
|
||||
module = __import__(safeFilepathEncode(filename[:-3]))
|
||||
function = getattr(module, "refresh", None)
|
||||
if not inspect.isfunction(function):
|
||||
raise ValueError("missing function 'refresh()'")
|
||||
spec = _argSpec(function)
|
||||
if len(spec.args) > len(spec.defaults or ()) or any(_ not in (getattr(spec, "kwonlydefaults", None) or {}) for _ in getattr(spec, "kwonlyargs", ())):
|
||||
raise ValueError("function 'refresh()' must be callable without arguments")
|
||||
except Exception:
|
||||
raise SqlmapSyntaxException("unable to load 'refresh()' from auth refresh script '%s'" % script)
|
||||
except (SqlmapFilePathException, SqlmapSyntaxException) as ex:
|
||||
errMsg = "%s. You can find a template script at '%s'" % (getSafeExString(ex), authrefresh.createTemplate())
|
||||
raise type(ex)(errMsg)
|
||||
|
||||
kb.authRefreshFunction = function
|
||||
|
||||
def _setThreads():
|
||||
if not isinstance(conf.threads, int) or conf.threads <= 0:
|
||||
conf.threads = 1
|
||||
@@ -2317,9 +2354,13 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.lastParserStatus = None
|
||||
|
||||
kb.locks = AttribDict()
|
||||
for _ in ("cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
|
||||
for _ in ("authRefresh", "cache", "connError", "count", "handlers", "hint", "identYwaf", "index", "io", "limit", "liveCookies", "log", "multibit", "prediction", "socket", "redirect", "request", "value"):
|
||||
kb.locks[_] = threading.Lock()
|
||||
|
||||
kb.authRefreshHeaders = {}
|
||||
kb.authRefreshVersion = 0
|
||||
kb.authRefreshFailed = False
|
||||
|
||||
kb.matchRatio = None
|
||||
kb.maxConnectionsFlag = False
|
||||
kb.trueLength = None
|
||||
@@ -2427,6 +2468,7 @@ def _setKnowledgeBaseAttributes(flushAll=True):
|
||||
kb.passwordMgr = None
|
||||
kb.postprocessFunctions = []
|
||||
kb.preprocessFunctions = []
|
||||
kb.authRefreshFunction = None
|
||||
kb.skipVulnHost = None
|
||||
kb.storeCrawlingChoice = None
|
||||
kb.tamperFunctions = []
|
||||
@@ -3171,6 +3213,7 @@ def init():
|
||||
_setTamperingFunctions()
|
||||
_setPreprocessFunctions()
|
||||
_setPostprocessFunctions()
|
||||
_setAuthRefreshFunction()
|
||||
_setTrafficOutputFP()
|
||||
_setupHTTPCollector()
|
||||
_setHttpOptions()
|
||||
|
||||
@@ -257,8 +257,9 @@ optDict = {
|
||||
"hexConvert": "boolean",
|
||||
"outputDir": "string",
|
||||
"parseErrors": "boolean",
|
||||
"postprocess": "string",
|
||||
"authRefresh": "string",
|
||||
"preprocess": "string",
|
||||
"postprocess": "string",
|
||||
"repair": "boolean",
|
||||
"reportJson": "string",
|
||||
"saveConfig": "string",
|
||||
|
||||
@@ -20,7 +20,7 @@ from lib.core.enums import OS
|
||||
from thirdparty import six
|
||||
|
||||
# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
|
||||
VERSION = "1.10.10.9"
|
||||
VERSION = "1.10.10.11"
|
||||
TYPE = "dev" if VERSION.count('.') > 2 and VERSION.split('.')[-1] != '0' else "stable"
|
||||
TYPE_COLORS = {"dev": 33, "stable": 90, "pip": 34}
|
||||
VERSION_STRING = "sqlmap/%s#%s" % ('.'.join(VERSION.split('.')[:-1]) if VERSION.count('.') > 2 and VERSION.split('.')[-1] == '0' else VERSION, TYPE)
|
||||
|
||||
@@ -60,6 +60,7 @@ class _ThreadData(threading.local):
|
||||
self.lastHTTPError = None
|
||||
self.lastRedirectMsg = None
|
||||
self.lastQueryDuration = 0
|
||||
self.authRefreshTime = 0
|
||||
self.lastPage = None
|
||||
self.lastRequestMsg = None
|
||||
self.lastRequestUID = 0
|
||||
|
||||
@@ -748,6 +748,9 @@ def cmdLineParser(argv=None):
|
||||
general.add_argument("--parse-errors", dest="parseErrors", action="store_true",
|
||||
help="Parse and display DBMS error messages from responses")
|
||||
|
||||
general.add_argument("--auth-refresh", dest="authRefresh",
|
||||
help="Run script to refresh auth on HTTP 401")
|
||||
|
||||
general.add_argument("--preprocess", dest="preprocess",
|
||||
help="Use given script(s) for preprocessing (request)")
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from lib.core.common import openFile
|
||||
from lib.core.data import conf
|
||||
from lib.core.data import kb
|
||||
from lib.core.data import logger
|
||||
from lib.core.enums import MKSTEMP_PREFIX
|
||||
from lib.core.exception import SqlmapConnectionException
|
||||
from lib.core.exception import SqlmapGenericException
|
||||
from thirdparty import six
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
|
||||
def createTemplate():
|
||||
handle, filename = tempfile.mkstemp(prefix=MKSTEMP_PREFIX.AUTH_REFRESH, suffix=".py")
|
||||
os.close(handle)
|
||||
|
||||
with openFile(filename, "w+") as f:
|
||||
f.write("#!/usr/bin/env\n\ndef refresh():\n # Obtain a fresh token here (use a timeout for login requests).\n # Custom headers also work, e.g. {'X-Auth-Token': token}.\n return {'Authorization': 'Bearer <fresh token>'}\n")
|
||||
with openFile(os.path.join(os.path.dirname(filename), "__init__.py"), "w+") as f:
|
||||
f.write("pass")
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def sameOrigin(first, second):
|
||||
# checkSameHost deliberately ignores schemes and ports, which is unsuitable for credentials.
|
||||
def origin(url):
|
||||
parts = _urllib.parse.urlsplit(url)
|
||||
return parts.scheme.lower(), parts.hostname, parts.port or (443 if parts.scheme.lower() == "https" else 80)
|
||||
|
||||
return origin(first) == origin(second)
|
||||
|
||||
|
||||
def apply(req):
|
||||
"""Overlay refreshed headers after preprocess hooks; return the credential generation sent."""
|
||||
|
||||
if not conf.authRefresh or not sameOrigin(req.get_full_url(), conf.url):
|
||||
return None
|
||||
|
||||
with kb.locks.authRefresh:
|
||||
if kb.authRefreshFailed:
|
||||
raise SqlmapGenericException("authentication refresh failed; cannot continue with expired credentials")
|
||||
for name, value in kb.authRefreshHeaders.items():
|
||||
# Hooks/raw request files can leave differently-cased copies of the same header.
|
||||
for headers in (req.headers, req.unredirected_hdrs):
|
||||
for key in list(headers):
|
||||
if key.lower() == name.lower():
|
||||
del headers[key]
|
||||
req.add_header(name, value)
|
||||
return kb.authRefreshVersion
|
||||
|
||||
|
||||
def refresh(version):
|
||||
"""Refresh once for all workers that sent the same expired credentials."""
|
||||
|
||||
with kb.locks.authRefresh:
|
||||
if kb.authRefreshFailed:
|
||||
raise SqlmapConnectionException("authentication refresh failed; cannot continue with expired credentials")
|
||||
if version != kb.authRefreshVersion:
|
||||
return
|
||||
|
||||
logger.info("refreshing authentication after HTTP 401")
|
||||
try:
|
||||
headers = kb.authRefreshFunction()
|
||||
if not isinstance(headers, dict) or not headers:
|
||||
raise ValueError()
|
||||
for name, value in headers.items():
|
||||
if not isinstance(name, six.string_types) or not re.match(r"\A[!#$%&'*+.^_`|~0-9A-Za-z-]+\Z", name):
|
||||
raise ValueError()
|
||||
if not isinstance(value, six.string_types) or not value or re.search(r"[\x00-\x1f\x7f]", value):
|
||||
raise ValueError()
|
||||
value.encode("latin-1")
|
||||
if name.lower() in ("host", "content-length", "transfer-encoding", "connection", "proxy-authorization"):
|
||||
raise ValueError()
|
||||
if len(set(_.lower() for _ in headers)) != len(headers):
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
kb.authRefreshFailed = True
|
||||
# Hook exceptions can contain tokens/passwords; do not include their text.
|
||||
raise SqlmapConnectionException("auth refresh hook failed or returned invalid headers. Expected a non-empty dictionary of authentication headers. You can find a template script at '%s'" % createTemplate())
|
||||
|
||||
kb.authRefreshHeaders = dict(headers)
|
||||
kb.authRefreshVersion += 1
|
||||
|
||||
|
||||
def stripHeaders(headers):
|
||||
"""Remove refreshed credentials before following a redirect to another origin."""
|
||||
|
||||
names = set(_.lower() for _ in kb.authRefreshHeaders)
|
||||
return dict((name, value) for name, value in headers.items() if name.lower() not in names)
|
||||
+29
-6
@@ -22,6 +22,7 @@ import time
|
||||
import traceback
|
||||
|
||||
from lib.request import websocket
|
||||
from lib.request import authrefresh
|
||||
from lib.request.websocket import WebSocketException
|
||||
|
||||
from lib.core.agent import agent
|
||||
@@ -469,6 +470,8 @@ class Connect(object):
|
||||
responseHeaders = None
|
||||
logHeaders = u""
|
||||
skipLogTraffic = False
|
||||
authRetry = False
|
||||
authVersion = None
|
||||
|
||||
raise404 = raise404 and not kb.ignoreNotFound
|
||||
|
||||
@@ -683,6 +686,9 @@ class Connect(object):
|
||||
else:
|
||||
post, headers = req.data, req.headers
|
||||
|
||||
authVersion = authrefresh.apply(req)
|
||||
headers = req.headers
|
||||
|
||||
requestHeaders += "\r\n".join(["%s: %s" % (u"-".join(_.capitalize() for _ in getUnicode(key).split(u'-')) if hasattr(key, "capitalize") else getUnicode(key), getUnicode(value)) for (key, value) in req.header_items()])
|
||||
|
||||
if not getRequestHeader(req, HTTP_HEADER.COOKIE) and conf.cj:
|
||||
@@ -730,12 +736,12 @@ class Connect(object):
|
||||
raise SqlmapMissingDependence("native HTTP/2 client does not support SOCKS proxies (omit '--http2' or use an HTTP proxy)")
|
||||
|
||||
try:
|
||||
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy)
|
||||
conn = http2OpenUrl(url, method or (HTTPMETHOD.POST if post is not None else HTTPMETHOD.GET), headers, post, timeout, follow_redirects=kb.choices.redirect != REDIRECTION.NO, proxy=h2proxy, sensitive_headers=tuple(kb.authRefreshHeaders) if conf.authRefresh else ())
|
||||
except IOError as ex:
|
||||
raise _http_client.HTTPException(getSafeExString(ex))
|
||||
else:
|
||||
if conn.code >= 400:
|
||||
raise _urllib.error.HTTPError(url, conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
|
||||
raise _urllib.error.HTTPError(conn.geturl(), conn.code, conn.msg, conn.info(), io.BytesIO(conn.read()))
|
||||
|
||||
requestMsg = re.sub(r" HTTP/[0-9.]+\r\n", " %s\r\n" % conn.http_version, requestMsg, count=1)
|
||||
|
||||
@@ -863,7 +869,7 @@ class Connect(object):
|
||||
page = None
|
||||
responseHeaders = None
|
||||
|
||||
if checking:
|
||||
if checking and not (ex.code == _http_client.UNAUTHORIZED and authVersion is not None):
|
||||
return None, None, None
|
||||
|
||||
try:
|
||||
@@ -887,7 +893,8 @@ class Connect(object):
|
||||
code = ex.code
|
||||
status = getUnicode(getattr(ex, "reason", None) or getSafeExString(ex).split(": ", 1)[-1])
|
||||
|
||||
kb.originalCode = kb.originalCode or code
|
||||
if not (code == _http_client.UNAUTHORIZED and authVersion is not None):
|
||||
kb.originalCode = kb.originalCode or code
|
||||
threadData.lastHTTPError = (threadData.lastRequestUID, code, status)
|
||||
kb.httpErrorCodes[code] = kb.httpErrorCodes.get(code, 0) + 1
|
||||
|
||||
@@ -913,7 +920,15 @@ class Connect(object):
|
||||
singleTimeLogMessage(errMsg, logging.CRITICAL)
|
||||
raise SystemExit
|
||||
|
||||
if ex.code not in (conf.ignoreCode or []):
|
||||
if ex.code == _http_client.UNAUTHORIZED and authVersion is not None and authrefresh.sameOrigin(ex.geturl(), url):
|
||||
if kwargs.get("authRetried"):
|
||||
with kb.locks.authRefresh:
|
||||
if authVersion == kb.authRefreshVersion:
|
||||
kb.authRefreshFailed = True
|
||||
raise SqlmapConnectionException("authentication refresh did not authorize the retried request (401)")
|
||||
authrefresh.refresh(authVersion)
|
||||
authRetry = True
|
||||
elif ex.code not in (conf.ignoreCode or []):
|
||||
if ex.code == TOO_MANY_REQUESTS_HTTP_CODE or (ex.code == _http_client.SERVICE_UNAVAILABLE and Connect._parseRetryAfter(responseHeaders) is not None):
|
||||
retried = Connect._rateLimitRetry(responseHeaders, ex.code, **kwargs)
|
||||
if retried is not None:
|
||||
@@ -1099,6 +1114,12 @@ class Connect(object):
|
||||
|
||||
socket.setdefaulttimeout(conf.timeout)
|
||||
|
||||
if authRetry:
|
||||
# Authentication latency is not evidence of a time-based SQL injection.
|
||||
threadData.authRefreshTime += calculateDeltaSeconds(start)
|
||||
kwargs["authRetried"] = True
|
||||
return Connect.getPage(**kwargs)
|
||||
|
||||
# Dirty patch for Python3.11.0a7 (e.g. https://github.com/sqlmapproject/sqlmap/issues/5091)
|
||||
if not sys.version.startswith("3.11."):
|
||||
if conf.retryOn and re.search(conf.retryOn, page or "", re.I):
|
||||
@@ -1751,6 +1772,8 @@ class Connect(object):
|
||||
|
||||
start = time.time()
|
||||
|
||||
authRefreshTime = threadData.authRefreshTime
|
||||
|
||||
if kb.nullConnection and not content and not response and not timeBasedCompare:
|
||||
noteResponseTime = False
|
||||
|
||||
@@ -1806,7 +1829,7 @@ class Connect(object):
|
||||
return value
|
||||
page, headers, code = Connect.getPage(url=_(kb.secondReq[0]), post=_(kb.secondReq[2]), method=kb.secondReq[1], cookie=kb.secondReq[3], silent=silent, auxHeaders=dict(auxHeaders, **dict(kb.secondReq[4])), response=response, raise404=False, ignoreTimeout=timeBasedCompare, refreshing=True)
|
||||
|
||||
threadData.lastQueryDuration = calculateDeltaSeconds(start)
|
||||
threadData.lastQueryDuration = max(0, calculateDeltaSeconds(start) - (threadData.authRefreshTime - authRefreshTime))
|
||||
|
||||
kb.originalCode = code if kb.originalCode is None else kb.originalCode
|
||||
kb.originalPage = page if kb.originalPage is None else kb.originalPage
|
||||
|
||||
@@ -1576,7 +1576,7 @@ def proxy_tuple(proxy, cred=None):
|
||||
|
||||
|
||||
def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redirects=True,
|
||||
max_redirects=10, proxy=None, verify=False, ssl_context=None):
|
||||
max_redirects=10, proxy=None, verify=False, ssl_context=None, sensitive_headers=()):
|
||||
req_headers = headers or {}
|
||||
current_method = method
|
||||
current_body = body
|
||||
@@ -1614,7 +1614,7 @@ def open_url(url, method="GET", headers=None, body=None, timeout=30, follow_redi
|
||||
old_parts = parts
|
||||
new_parts = urlsplit(new_url)
|
||||
if _origin_tuple(old_parts) != _origin_tuple(new_parts):
|
||||
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS)
|
||||
req_headers = _headers_without(req_headers, _SENSITIVE_REDIRECT_FIELDS.union(_as_bytes(_).lower() for _ in sensitive_headers))
|
||||
|
||||
method_bytes = _as_bytes(current_method)
|
||||
change_to_get = status == 303 and method_bytes != b"HEAD"
|
||||
|
||||
@@ -32,6 +32,7 @@ from lib.core.settings import MAX_TOTAL_REDIRECTIONS
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.request.basic import decodePage
|
||||
from lib.request.basic import parseResponse
|
||||
from lib.request import authrefresh
|
||||
from thirdparty import six
|
||||
from thirdparty.six.moves import http_client as _http_client
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
@@ -176,6 +177,9 @@ class SmartRedirectHandler(_urllib.request.HTTPRedirectHandler):
|
||||
req.headers[HTTP_HEADER.COOKIE] = delimiter.join("%s=%s" % (key, cookies[key]) for key in cookies)
|
||||
|
||||
try:
|
||||
if conf.authRefresh and not authrefresh.sameOrigin(req.get_full_url(), redurl):
|
||||
req.headers = authrefresh.stripHeaders(req.headers)
|
||||
req.unredirected_hdrs = authrefresh.stripHeaders(req.unredirected_hdrs)
|
||||
result = _urllib.request.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
|
||||
except _urllib.error.HTTPError as ex:
|
||||
result = error = ex
|
||||
|
||||
@@ -809,6 +809,9 @@ outputDir =
|
||||
# Valid: True or False
|
||||
parseErrors = False
|
||||
|
||||
# Run a Python refresh() script returning replacement headers on HTTP 401.
|
||||
authRefresh =
|
||||
|
||||
# Use given script(s) for preprocessing of request.
|
||||
preprocess =
|
||||
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
|
||||
See the file 'LICENSE' for copying permission
|
||||
|
||||
Authentication refresh hook loading and replay against a local HTTP server.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _testutils import bootstrap
|
||||
bootstrap()
|
||||
|
||||
from lib.core.data import conf, kb, paths, cmdLineOptions
|
||||
from lib.core.enums import MKSTEMP_PREFIX, REDIRECTION
|
||||
from lib.core.exception import SqlmapConnectionException, SqlmapFilePathException, SqlmapSyntaxException
|
||||
from lib.core.option import _setAuthRefreshFunction, _setKnowledgeBaseAttributes
|
||||
from lib.core.threads import getCurrentThreadData
|
||||
from lib.request import authrefresh
|
||||
from lib.request.connect import Connect
|
||||
from lib.request.redirecthandler import SmartRedirectHandler
|
||||
from thirdparty.six.moves import BaseHTTPServer, socketserver
|
||||
from thirdparty.six.moves import urllib as _urllib
|
||||
|
||||
_GET_PAGE = Connect.__dict__['getPage']
|
||||
_QUERY_PAGE = Connect.__dict__['queryPage']
|
||||
|
||||
|
||||
def _assertTemplate(testCase, exception):
|
||||
match = re.search(r"template script at '([^']+)'", str(exception))
|
||||
testCase.assertIsNotNone(match)
|
||||
path = match.group(1)
|
||||
try:
|
||||
testCase.assertTrue(os.path.basename(path).startswith(MKSTEMP_PREFIX.AUTH_REFRESH))
|
||||
testCase.assertTrue(os.path.isfile(os.path.join(os.path.dirname(path), '__init__.py')))
|
||||
with open(path) as stream:
|
||||
source = stream.read()
|
||||
namespace = {}
|
||||
eval(compile(source, path, 'exec'), namespace)
|
||||
testCase.assertEqual(namespace['refresh'](), {'Authorization': 'Bearer <fresh token>'})
|
||||
testCase.assertNotIn(paths.SQLMAP_EXTRAS_PATH, path)
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class _Server(socketserver.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class _Handler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
self.do_POST()
|
||||
|
||||
def do_POST(self):
|
||||
body = self.rfile.read(int(self.headers.get('Content-Length', '0')))
|
||||
token = self.headers.get('X-Auth-Token')
|
||||
if self.path == '/refresh':
|
||||
self.server.refreshes += 1
|
||||
code, body = 200, b'{"token":"fresh"}'
|
||||
else:
|
||||
with self.server.lock:
|
||||
self.server.requests.append((self.path, body, token))
|
||||
if token != self.server.token:
|
||||
self.server.expired += 1
|
||||
if self.server.expired >= 2:
|
||||
self.server.bothExpired.set()
|
||||
if token != self.server.token and self.server.syncExpired:
|
||||
self.server.bothExpired.wait(3)
|
||||
code = 200 if token == self.server.token else 401
|
||||
body = b'ok' if code == 200 else b'expired'
|
||||
self.send_response(302 if self.path == '/redirect' and code == 200 else code)
|
||||
if self.path == '/redirect' and code == 200:
|
||||
self.send_header('Location', self.server.redirect)
|
||||
self.send_header('Content-Type', 'text/plain; charset=utf-8')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
class TestAuthRefresh(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.conf = dict(conf)
|
||||
self.cmdLineOptions = dict(cmdLineOptions)
|
||||
cmdLineOptions.method = None
|
||||
self.kb = dict(kb)
|
||||
self.threadData = dict(getCurrentThreadData().__dict__)
|
||||
getCurrentThreadData().__dict__.clear()
|
||||
getCurrentThreadData().reset()
|
||||
self.timeout = socket.getdefaulttimeout()
|
||||
self.urlopen = _urllib.request.urlopen
|
||||
# Other engine tests replace these descriptors; use the real methods even when an
|
||||
# earlier Python 2 fixture restored a bare function instead of a staticmethod.
|
||||
self.methods = Connect.__dict__['getPage'], Connect.__dict__['queryPage']
|
||||
Connect.getPage, Connect.queryPage = _GET_PAGE, _QUERY_PAGE
|
||||
_setKnowledgeBaseAttributes(False)
|
||||
kb.wizardMode = True
|
||||
self.server = _Server(('127.0.0.1', 0), _Handler)
|
||||
self.server.requests = []
|
||||
self.server.refreshes = 0
|
||||
self.server.token = 'fresh'
|
||||
self.server.expired = 0
|
||||
self.server.syncExpired = False
|
||||
self.server.bothExpired = threading.Event()
|
||||
self.server.lock = threading.Lock()
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.daemon = True
|
||||
self.thread.start()
|
||||
conf.url = 'http://127.0.0.1:%d/test' % self.server.server_port
|
||||
conf.hostname, conf.scheme, conf.port = '127.0.0.1', 'http', self.server.server_port
|
||||
conf.path = '/test'
|
||||
conf.httpHeaders = [('X-Auth-Token', 'expired')]
|
||||
conf.parameters = {}
|
||||
conf.data = None
|
||||
conf.abortCode = []
|
||||
conf.ignoreCode = []
|
||||
conf.authRefresh = 'test_refresh.py'
|
||||
conf.batch = True
|
||||
conf.keepAlive = False
|
||||
conf.retryOn = None
|
||||
conf.timeout = 5
|
||||
conf.ignoreRedirects = False
|
||||
conf.scope = None
|
||||
kb.choices.redirect = REDIRECTION.YES
|
||||
_urllib.request.urlopen = _urllib.request.build_opener(_urllib.request.ProxyHandler({}), SmartRedirectHandler()).open
|
||||
|
||||
def refresh():
|
||||
# A real, application-specific token exchange outside sqlmap's request engine.
|
||||
response = _urllib.request.urlopen(conf.url.replace('/test', '/refresh'), timeout=3)
|
||||
try:
|
||||
return {'X-Auth-Token': json.loads(response.read().decode('ascii'))['token']}
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
kb.authRefreshFunction = refresh
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.thread.join(5)
|
||||
_urllib.request.urlopen = self.urlopen
|
||||
Connect.getPage, Connect.queryPage = self.methods
|
||||
socket.setdefaulttimeout(self.timeout)
|
||||
conf.clear()
|
||||
conf.update(self.conf)
|
||||
cmdLineOptions.clear()
|
||||
cmdLineOptions.update(self.cmdLineOptions)
|
||||
kb.clear()
|
||||
kb.update(self.kb)
|
||||
getCurrentThreadData().__dict__.clear()
|
||||
getCurrentThreadData().__dict__.update(self.threadData)
|
||||
|
||||
def test_expiry_mid_scan_preserves_payload_and_reuses_headers(self):
|
||||
self.server.token = 'expired'
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
self.server.token = 'fresh'
|
||||
payload = b'id=1%20AND%201%3D1&other=keep'
|
||||
self.assertEqual(Connect.getPage(get='marker=123', post=payload)[2], 200)
|
||||
self.assertEqual(self.server.requests[-2:], [('/test?marker=123', payload, 'expired'), ('/test?marker=123', payload, 'fresh')])
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
self.assertEqual(getCurrentThreadData().lastCode, 200)
|
||||
self.assertEqual(getCurrentThreadData().lastPage, 'ok')
|
||||
self.assertNotEqual(kb.originalCode, 401)
|
||||
|
||||
def test_second_401_stops_after_one_replay(self):
|
||||
kb.authRefreshFunction = lambda: {'X-Auth-Token': 'still-expired'}
|
||||
with self.assertRaises(SqlmapConnectionException):
|
||||
Connect.getPage()
|
||||
self.assertEqual(len(self.server.requests), 2)
|
||||
self.assertTrue(kb.authRefreshFailed)
|
||||
|
||||
def test_abort_code_takes_precedence(self):
|
||||
conf.abortCode = [401]
|
||||
with self.assertRaises(SystemExit):
|
||||
Connect.getPage()
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
self.assertEqual(len(self.server.requests), 1)
|
||||
|
||||
def test_without_option_preserves_401_behavior(self):
|
||||
conf.authRefresh = None
|
||||
with self.assertRaises(SqlmapConnectionException):
|
||||
Connect.getPage()
|
||||
self.assertEqual(self.server.refreshes, 0)
|
||||
|
||||
def test_concurrent_401s_share_one_refresh(self):
|
||||
self.server.syncExpired = True
|
||||
results, errors = [], []
|
||||
|
||||
def worker():
|
||||
try:
|
||||
results.append(Connect.getPage()[2])
|
||||
except Exception as ex:
|
||||
errors.append(ex)
|
||||
|
||||
workers = [threading.Thread(target=worker) for _ in range(2)]
|
||||
for workerThread in workers:
|
||||
workerThread.start()
|
||||
for workerThread in workers:
|
||||
workerThread.join(10)
|
||||
self.assertFalse(workerThread.is_alive())
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(results, [200, 200])
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
self.assertEqual(self.server.expired, 2)
|
||||
|
||||
def test_invalid_hook_results_are_rejected_without_leaking_secrets(self):
|
||||
for result in (None, {}, 'secret-token', {'X-Token': 'secret\r\nInjected: value'}, {'Host': 'elsewhere'}, {'X-Token': 42}, {'X-Token': 'secret', 'x-token': 'other'}):
|
||||
kb.authRefreshFailed = False
|
||||
kb.authRefreshFunction = lambda: result
|
||||
with self.assertRaises(SqlmapConnectionException) as caught:
|
||||
authrefresh.refresh(0)
|
||||
self.assertNotIn('secret', str(caught.exception))
|
||||
_assertTemplate(self, caught.exception)
|
||||
self.assertEqual(kb.authRefreshVersion, 0)
|
||||
|
||||
def test_hook_exception_is_sanitized(self):
|
||||
def refresh():
|
||||
raise ValueError('password=secret')
|
||||
kb.authRefreshFunction = refresh
|
||||
with self.assertRaises(SqlmapConnectionException) as caught:
|
||||
Connect.getPage()
|
||||
self.assertNotIn('secret', str(caught.exception))
|
||||
_assertTemplate(self, caught.exception)
|
||||
self.assertEqual(len(self.server.requests), 1)
|
||||
|
||||
def test_credentials_are_scoped_to_scheme_host_and_port(self):
|
||||
authrefresh.refresh(0)
|
||||
for url in ('http://elsewhere/test', 'https://127.0.0.1/test', 'http://127.0.0.1:1/test'):
|
||||
req = _urllib.request.Request(url)
|
||||
self.assertIsNone(authrefresh.apply(req))
|
||||
self.assertEqual(req.headers, {})
|
||||
self.assertTrue(authrefresh.sameOrigin('https://example.com', 'https://example.com:443/path'))
|
||||
|
||||
def test_refresh_overrides_stale_preprocess_headers(self):
|
||||
kb.preprocessFunctions = [lambda req: req.add_header('X-Auth-Token', 'expired')]
|
||||
self.assertEqual(Connect.getPage()[2], 200)
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
|
||||
def test_redirect_to_another_port_does_not_receive_refreshed_token(self):
|
||||
authrefresh.refresh(0)
|
||||
other = _Server(('127.0.0.1', 0), _Handler)
|
||||
other.requests, other.token, other.expired = [], None, 0
|
||||
other.lock = threading.Lock()
|
||||
other.syncExpired = False
|
||||
thread = threading.Thread(target=other.serve_forever)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
try:
|
||||
self.server.redirect = 'http://127.0.0.1:%d/landing' % other.server_port
|
||||
Connect.getPage(url=conf.url.replace('/test', '/redirect'))
|
||||
self.assertEqual(other.requests, [('/landing', b'', None)])
|
||||
self.assertEqual(self.server.refreshes, 1)
|
||||
finally:
|
||||
other.shutdown()
|
||||
other.server_close()
|
||||
thread.join(5)
|
||||
|
||||
def test_auth_refresh_latency_is_excluded_from_query_timing(self):
|
||||
import lib.request.connect as connect
|
||||
originalPage, originalDelta = Connect.getPage, connect.calculateDeltaSeconds
|
||||
|
||||
def getPage(**kwargs):
|
||||
getCurrentThreadData().authRefreshTime += 12
|
||||
return 'ok', None, 200
|
||||
|
||||
Connect.getPage = staticmethod(getPage)
|
||||
connect.calculateDeltaSeconds = lambda start: 15
|
||||
try:
|
||||
Connect.queryPage(content=True)
|
||||
self.assertEqual(getCurrentThreadData().lastQueryDuration, 3)
|
||||
finally:
|
||||
Connect.getPage = staticmethod(originalPage)
|
||||
connect.calculateDeltaSeconds = originalDelta
|
||||
|
||||
|
||||
class TestAuthRefreshLoader(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.directory = tempfile.mkdtemp(prefix='sqlmap-authrefresh-')
|
||||
self.oldOption = conf.authRefresh
|
||||
self.oldFunction = kb.authRefreshFunction
|
||||
self.oldPath = list(sys.path)
|
||||
|
||||
def tearDown(self):
|
||||
conf.authRefresh = self.oldOption
|
||||
kb.authRefreshFunction = self.oldFunction
|
||||
sys.path[:] = self.oldPath
|
||||
for name in list(sys.modules):
|
||||
if name.startswith('authrefresh_test_'):
|
||||
del sys.modules[name]
|
||||
shutil.rmtree(self.directory)
|
||||
|
||||
def script(self, name, content):
|
||||
path = os.path.join(self.directory, 'authrefresh_test_%s.py' % name)
|
||||
with open(path, 'w') as stream:
|
||||
stream.write(content)
|
||||
conf.authRefresh = path
|
||||
|
||||
def test_load_does_not_call_refresh(self):
|
||||
self.script('valid', "def refresh():\n raise AssertionError('must only run on 401')\n")
|
||||
_setAuthRefreshFunction()
|
||||
self.assertTrue(callable(kb.authRefreshFunction))
|
||||
|
||||
def test_missing_invalid_and_empty_scripts_point_to_template(self):
|
||||
for name, source in (('empty', ''), ('wrong', 'def other():\n pass\n'), ('args', 'def refresh(req):\n pass\n'), ('syntax', 'def refresh(:')):
|
||||
self.script(name, source)
|
||||
with self.assertRaises(SqlmapSyntaxException) as caught:
|
||||
_setAuthRefreshFunction()
|
||||
_assertTemplate(self, caught.exception)
|
||||
conf.authRefresh = os.path.join(self.directory, 'missing.py')
|
||||
with self.assertRaises(SqlmapFilePathException) as caught:
|
||||
_setAuthRefreshFunction()
|
||||
_assertTemplate(self, caught.exception)
|
||||
|
||||
def test_help_is_one_line(self):
|
||||
output = subprocess.check_output([sys.executable, os.path.join(paths.SQLMAP_ROOT_PATH, 'sqlmap.py'), '-hh'])
|
||||
lines = output.decode('utf-8').splitlines()
|
||||
matches = [i for i, line in enumerate(lines) if '--auth-refresh' in line]
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertIn('HTTP 401', lines[matches[0]])
|
||||
self.assertLessEqual(len(lines[matches[0]]), 78)
|
||||
for option in ('--preprocess', '--postprocess'):
|
||||
self.assertLess(matches[0], next(i for i, line in enumerate(lines) if option in line))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -114,16 +114,19 @@ class TestConfigFileRoundTrip(unittest.TestCase):
|
||||
conf.url = "http://127.0.0.1:1/?id=1"
|
||||
conf.commonTables = True
|
||||
conf.getLinks = True
|
||||
conf.authRefresh = "refresh_auth.py"
|
||||
conf.tablePrefix = "zzz"
|
||||
saveConfig(conf, path)
|
||||
|
||||
conf.url, conf.commonTables, conf.tablePrefix = None, False, "overwritten"
|
||||
conf.getLinks = False
|
||||
conf.authRefresh = None
|
||||
configFileParser(path)
|
||||
|
||||
self.assertEqual(conf.url, "http://127.0.0.1:1/?id=1")
|
||||
self.assertTrue(conf.commonTables)
|
||||
self.assertTrue(conf.getLinks)
|
||||
self.assertEqual(conf.authRefresh, "refresh_auth.py")
|
||||
self.assertEqual(conf.tablePrefix, "zzz")
|
||||
finally:
|
||||
conf.clear()
|
||||
|
||||
@@ -699,6 +699,17 @@ class TestRedirectHandling(unittest.TestCase):
|
||||
self.assertEqual(calls[0][1], 8443)
|
||||
self.assertEqual(calls[0][4], "xn--tst-qla.example:8443")
|
||||
|
||||
def test_cross_origin_redirect_strips_custom_auth_headers(self):
|
||||
responses = [(307, [(b"location", b"https://other.test/final")], b""), (200, [], b"ok")]
|
||||
_, calls = self._run_with_fake_exchange(
|
||||
responses, url="https://example.test/start",
|
||||
headers={"X-Auth-Token": "secret", "X-Keep": "yes"},
|
||||
sensitive_headers=("X-Auth-Token",),
|
||||
)
|
||||
names = [key.lower() for key, _ in calls[1][5]]
|
||||
self.assertNotIn("x-auth-token", names)
|
||||
self.assertIn("x-keep", names)
|
||||
|
||||
|
||||
class TestPoolRetries(unittest.TestCase):
|
||||
class _Connection(object):
|
||||
|
||||
Reference in New Issue
Block a user