@@ -539,26 +539,35 @@ def validateChar(idx, value):
539539 forgedPayload = validationPayload .replace (markingValue , unescapedCharValue )
540540 forgedPayload = safeStringFormat (forgedPayload , (expressionUnescaped , idx ))
541541
542- result = not Request .queryPage (forgedPayload , timeBasedCompare = timeBasedCompare , raise404 = False )
543-
544- if result and getTechniqueData () is not None :
545- trueCode , falseCode = getTechniqueData ().trueCode , getTechniqueData ().falseCode
546- if timeBasedCompare :
547- if trueCode :
548- result = threadData .lastCode == trueCode
549- if not result :
550- warnMsg = "detected HTTP code '%s' in validation phase is differing from expected '%s'" % (threadData .lastCode , trueCode )
551- singleTimeWarnMessage (warnMsg )
552- # A boolean validation confirmed under an UNEXPECTED HTTP code (a transient 5xx/403/429/..
553- # landing on the validation request itself) is not trustworthy - fail it so the character is
554- # re-extracted, riding out the blip. On a clean target every code is true/false -> no-op.
555- elif threadData .lastCode is not None and any ((trueCode , falseCode )) and threadData .lastCode not in (trueCode , falseCode ):
556- result = False
557- singleTimeWarnMessage ("unexpected HTTP code '%s' during validation phase; will re-extract" % threadData .lastCode )
542+ def _check ():
543+ result = not Request .queryPage (forgedPayload , timeBasedCompare = timeBasedCompare , raise404 = False )
558544
559- incrementCounter (getTechnique ())
545+ if result and getTechniqueData () is not None :
546+ trueCode , falseCode = getTechniqueData ().trueCode , getTechniqueData ().falseCode
547+ if timeBasedCompare :
548+ if trueCode :
549+ result = threadData .lastCode == trueCode
550+ if not result :
551+ warnMsg = "detected HTTP code '%s' in validation phase is differing from expected '%s'" % (threadData .lastCode , trueCode )
552+ singleTimeWarnMessage (warnMsg )
553+ # A boolean validation confirmed under an UNEXPECTED HTTP code (a transient 5xx/403/429/..
554+ # landing on the validation request itself) is not trustworthy - fail it so the character is
555+ # re-extracted, riding out the blip. On a clean target every code is true/false -> no-op.
556+ elif threadData .lastCode is not None and any ((trueCode , falseCode )) and threadData .lastCode not in (trueCode , falseCode ):
557+ result = False
558+ singleTimeWarnMessage ("unexpected HTTP code '%s' during validation phase; will re-extract" % threadData .lastCode )
560559
561- return result
560+ incrementCounter (getTechnique ())
561+ return result
562+
563+ # Adaptive majority vote: once jitter has been observed this run a single re-check can itself
564+ # be corrupted, so confirm the character by best-of-3 independent re-checks. Confidence-gated
565+ # -> exact no-op (single check) on a clean run or before any jitter is seen.
566+ if kb .get ("jitterSeen" ):
567+ votes = [_check () for _ in xrange (3 )]
568+ return votes .count (True ) >= 2
569+
570+ return _check ()
562571
563572 def huffmanChar (idx ):
564573 """
@@ -823,6 +832,9 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,
823832 unexpectedResponse = True
824833 singleTimeWarnMessage ("unexpected response content detected. Will use (extra) validation step in similar cases" )
825834
835+ if unexpectedCode or unexpectedResponse :
836+ kb .jitterSeen = True # latch: jitter observed -> escalate validateChar to a vote
837+
826838 if result :
827839 minValue = posValue
828840
@@ -862,7 +874,12 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,
862874 retVal = minValue + 1
863875
864876 if retVal in originalTbl or (retVal == ord ('\n ' ) and CHAR_INFERENCE_MARK in payload ):
865- if (timeBasedCompare or unexpectedCode or unexpectedResponse ) and kb .get ("timeless" ) is None and not validateChar (idx , retVal ):
877+ # Once jitter has been observed this run, confirm EVERY resolved character
878+ # (via the best-of-3 vote in validateChar), not only visibly-glitched ones:
879+ # a same-HTTP-code junk that resembles a model corrupts a bit INVISIBLY, and a
880+ # single-char value (e.g. a boolean --is-dba) has no other char to later trip
881+ # detection. Confidence-gated on kb.jitterSeen -> no-op on a clean target.
882+ if (timeBasedCompare or unexpectedCode or unexpectedResponse or kb .get ("jitterSeen" )) and kb .get ("timeless" ) is None and not validateChar (idx , retVal ):
866883 if restricted :
867884 # the character fell outside this column's observed range - re-extract
868885 # over the full charset (not timing noise, so no delay increase / retry count)
@@ -871,7 +888,10 @@ def getChar(idx, charTbl=None, continuousOrder=True, expand=charsetType is None,
871888 kb .originalTimeDelay = conf .timeSec
872889
873890 threadData .validationRun = 0
874- if (retried or 0 ) < MAX_REVALIDATION_STEPS :
891+ kb .jitterSeen = True # a needed re-extraction is itself a jitter signal (covers time-based)
892+ # under detected jitter, allow more retries so a transient burst can't exhaust the budget
893+ maxRevalidation = MAX_REVALIDATION_STEPS * 3 if kb .jitterSeen else MAX_REVALIDATION_STEPS
894+ if (retried or 0 ) < maxRevalidation :
875895 errMsg = "invalid character detected. retrying.."
876896 logger .error (errMsg )
877897
0 commit comments