Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/controller/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,12 @@ def genCmpPayload():
injection.data[stype].trueCode = trueCode
injection.data[stype].falseCode = falseCode

# reference bodies for inference.py's "resembles neither TRUE nor FALSE model"
# anomaly guard (runtime-only; lets a transient same-HTTP-code junk response
# trigger a validateChar re-check during boolean extraction)
if method == PAYLOAD.METHOD.COMPARISON:
kb.trueTemplate, kb.falseTemplate = truePage, falsePage

injection.conf.textOnly = conf.textOnly
injection.conf.titles = conf.titles
injection.conf.code = conf.code
Expand Down
4 changes: 4 additions & 0 deletions lib/core/option.py
Original file line number Diff line number Diff line change
Expand Up @@ -2305,6 +2305,10 @@ def _setKnowledgeBaseAttributes(flushAll=True):
kb.pageTemplate = None
kb.pageTemplates = dict()
kb.pageEncoding = DEFAULT_PAGE_ENCODING

# calibrated TRUE/FALSE reference bodies for the boolean same-HTTP-code anomaly guard (inference.py)
kb.trueTemplate = None
kb.falseTemplate = None
kb.pageStable = None
kb.pageStructurallyStable = None
kb.partRun = None
Expand Down
7 changes: 6 additions & 1 deletion lib/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from thirdparty import six

# sqlmap version (<major>.<minor>.<month>.<monthly commit>)
VERSION = "1.10.7.240"
VERSION = "1.10.7.242"
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)
Expand Down Expand Up @@ -103,6 +103,11 @@
LOWER_RATIO_BOUND = 0.02
UPPER_RATIO_BOUND = 0.98

# Minimum similarity at which a boolean extraction response is judged to "resemble" the calibrated
# TRUE or FALSE model. A response resembling NEITHER (a transient same-HTTP-code junk page: WAF/CDN
# interstitial, captcha, maintenance, empty/truncated body) triggers an extra validateChar re-check.
BOOLEAN_MODEL_MATCH_RATIO = 0.9

# Number of candidate names probed per request while mining for hidden parameters ('--mine-params')
PARAMETER_MINING_BUCKET_SIZE = 25

Expand Down
57 changes: 51 additions & 6 deletions lib/techniques/blind/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import division

import difflib
import heapq
import re
import time
Expand All @@ -26,6 +27,7 @@
from lib.core.common import getTechniqueData
from lib.core.common import getText
from lib.core.common import predictValue
from lib.core.common import removeDynamicContent
from lib.core.common import hashDBRetrieve
from lib.core.common import hashDBWrite
from lib.core.common import incrementCounter
Expand All @@ -45,6 +47,7 @@
from lib.core.exception import SqlmapThreadException
from lib.core.exception import SqlmapUnsupportedFeatureException
from lib.core.wordlist import Wordlist
from lib.core.settings import BOOLEAN_MODEL_MATCH_RATIO
from lib.core.settings import CHAR_INFERENCE_MARK
from lib.core.settings import HUFFMAN_PROBE_LIMIT
from lib.core.settings import HUFFMAN_PRIOR_WEIGHTS
Expand Down Expand Up @@ -256,6 +259,31 @@ def oracleReliabilityLitmus(expressionUnescaped, value, timeBasedCompare):

return bool(mustBeTrue) and not bool(mustBeFalse)

def _resemblesNeitherModel(page):
"""
Returns True when a boolean extraction response resembles NEITHER the calibrated TRUE nor FALSE
model (kb.trueTemplate / kb.falseTemplate). A transient response that keeps the expected HTTP code
but swaps the body for junk (WAF/CDN interstitial, captcha, maintenance banner, empty/truncated
page) is invisible to the HTTP-code check, so it is flagged here for an extra validateChar re-check.

This only ever ADDS a re-validation - it never changes a decided bit - so it is a safe no-op on a
clean target (every response resembles its own model) and when no models were recorded (a resumed
session, or a non-boolean technique).
"""

refs = [_ for _ in (kb.trueTemplate, kb.falseTemplate) if _]
if not refs:
return False
if page is None:
return True

cleaned = removeDynamicContent(page)
for ref in refs:
if difflib.SequenceMatcher(None, removeDynamicContent(ref), cleaned).quick_ratio() >= BOOLEAN_MODEL_MATCH_RATIO:
return False

return True

def bisection(payload, expression, length=None, charsetType=None, firstChar=None, lastChar=None, dump=False):
"""
Bisection algorithm that can be used to perform blind SQL injection
Expand Down Expand Up @@ -513,11 +541,20 @@ def validateChar(idx, value):

result = not Request.queryPage(forgedPayload, timeBasedCompare=timeBasedCompare, raise404=False)

if result and timeBasedCompare and getTechniqueData().trueCode:
result = threadData.lastCode == getTechniqueData().trueCode
if not result:
warnMsg = "detected HTTP code '%s' in validation phase is differing from expected '%s'" % (threadData.lastCode, getTechniqueData().trueCode)
singleTimeWarnMessage(warnMsg)
if result and getTechniqueData() is not None:
trueCode, falseCode = getTechniqueData().trueCode, getTechniqueData().falseCode
if timeBasedCompare:
if trueCode:
result = threadData.lastCode == trueCode
if not result:
warnMsg = "detected HTTP code '%s' in validation phase is differing from expected '%s'" % (threadData.lastCode, trueCode)
singleTimeWarnMessage(warnMsg)
# A boolean validation confirmed under an UNEXPECTED HTTP code (a transient 5xx/403/429/..
# landing on the validation request itself) is not trustworthy - fail it so the character is
# re-extracted, riding out the blip. On a clean target every code is true/false -> no-op.
elif threadData.lastCode is not None and any((trueCode, falseCode)) and threadData.lastCode not in (trueCode, falseCode):
result = False
singleTimeWarnMessage("unexpected HTTP code '%s' during validation phase; will re-extract" % threadData.lastCode)

incrementCounter(getTechnique())

Expand Down Expand Up @@ -705,6 +742,7 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,
firstCheck = False
lastCheck = False
unexpectedCode = False
unexpectedResponse = False

if continuousOrder:
while len(charTbl) > 1:
Expand Down Expand Up @@ -778,6 +816,13 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,

singleTimeWarnMessage(warnMsg)

# same-HTTP-code body anomaly (WAF/CDN interstitial, captcha, maintenance, empty
# or truncated body) - invisible to the code check above, so re-validate when the
# response resembles neither calibrated model
elif not unexpectedResponse and not kb.nullConnection and _resemblesNeitherModel(threadData.lastPage):
unexpectedResponse = True
singleTimeWarnMessage("unexpected response content detected. Will use (extra) validation step in similar cases")

if result:
minValue = posValue

Expand Down Expand Up @@ -817,7 +862,7 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,
retVal = minValue + 1

if retVal in originalTbl or (retVal == ord('\n') and CHAR_INFERENCE_MARK in payload):
if (timeBasedCompare or unexpectedCode) and kb.get("timeless") is None and not validateChar(idx, retVal):
if (timeBasedCompare or unexpectedCode or unexpectedResponse) and kb.get("timeless") is None and not validateChar(idx, retVal):
if restricted:
# the character fell outside this column's observed range - re-extract
# over the full charset (not timing noise, so no delay increase / retry count)
Expand Down
Loading