Skip to content

Commit 2a76775

Browse files
authored
Merge pull request #2911 from blacklanternsecurity/paddingoracle-fix
improve padding oracle detection
2 parents f13e9b7 + 1b0d88b commit 2a76775

2 files changed

Lines changed: 182 additions & 14 deletions

File tree

bbot/modules/lightfuzz/submodules/crypto.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -232,37 +232,56 @@ async def padding_oracle_execute(self, original_data, encoding, block_size, cook
232232
else:
233233
baseline_byte = b"\x00" # set the baseline byte to 0x00
234234
starting_pos = 1 # set the starting position to 1
235-
# first obtain
235+
236+
baseline_probe_value = self.format_agnostic_encode(
237+
ivblock + paddingblock[:-1] + baseline_byte + datablock, encoding
238+
)
236239
baseline = self.compare_baseline(
237240
self.event.data["type"],
238-
self.format_agnostic_encode(ivblock + paddingblock[:-1] + baseline_byte + datablock, encoding),
241+
baseline_probe_value,
239242
cookies,
240243
)
241244
differ_count = 0
242245
# for each possible byte value, send a probe and check if the response is different
243246
for i in range(starting_pos, starting_pos + 254):
244247
byte = bytes([i])
248+
probe_value = self.format_agnostic_encode(ivblock + paddingblock[:-1] + byte + datablock, encoding)
245249
oracle_probe = await self.compare_probe(
246250
baseline,
247251
self.event.data["type"],
248-
self.format_agnostic_encode(ivblock + paddingblock[:-1] + byte + datablock, encoding),
252+
probe_value,
249253
cookies,
250254
)
251255
# oracle_probe[0] will be false if the response is different - oracle_probe[1] stores what aspect of the response is different (headers, body, code)
252256
if oracle_probe[0] is False and "body" in oracle_probe[1]:
257+
# When the server reflects submitted values or reveals decrypted data, every probe will differ in the body. Strip the known probe values from both responses and re-compare.
258+
stripped_baseline = baseline.baseline.text
259+
stripped_probe = oracle_probe[3].text
260+
for encoded_baseline, encoded_probe in [
261+
(baseline_probe_value, probe_value),
262+
(baseline_probe_value.replace("+", " "), probe_value.replace("+", " ")),
263+
(quote(baseline_probe_value), quote(probe_value)),
264+
]:
265+
stripped_baseline = stripped_baseline.replace(encoded_baseline, "")
266+
stripped_probe = stripped_probe.replace(encoded_probe, "")
267+
if stripped_baseline == stripped_probe:
268+
continue
269+
# If the server reveals decrypted data, the response may differ by only a few bytes (the varying decrypted byte). Tolerate small character-level differences.
270+
if len(stripped_baseline) == len(stripped_probe):
271+
char_diffs = sum(1 for a, b in zip(stripped_baseline, stripped_probe) if a != b)
272+
if char_diffs <= 5:
273+
continue
253274
differ_count += 1
254-
255-
if i == 2:
256-
if possible_first_byte is True:
257-
# Thats two results which appear "different". Since this is the first run, it's entirely possible \x00 was the correct padding.
258-
# We will break from this loop and redo it with the last byte as the baseline instead of the first
259-
return None
260-
else:
261-
# Now that we have tried the run twice, we know it can't be because the first byte was the correct padding, and we know it is not vulnerable
262-
return False
263-
# A padding oracle vulnerability will produce exactly one different response, and no more, so this is likely a real padding oracle
264-
if differ_count == 1:
275+
self.debug(f"padding_oracle_execute: finished loop. differ_count={differ_count}")
276+
# A padding oracle vulnerability can produce a small number of different responses.
277+
# The correct \x01 padding byte always differs, but also, multi-byte padding values (\x02\x02, \x03\x03\x03, etc.) can also produce valid padding if the intermediate state randomly aligns. At most 'block_size' of such values are possible.
278+
if 1 <= differ_count <= block_size:
265279
return True
280+
# If too many probes differ, the baseline byte may have been the correct padding byte (1/255 chance).
281+
# In that case, the baseline response represents "valid padding" and nearly all probes appear different.
282+
# Retry with a different baseline byte to rule this out.
283+
if possible_first_byte and differ_count > block_size:
284+
return None
266285
return False
267286

268287
async def padding_oracle(self, probe_value, cookies):

bbot/test/test_step_2/module_tests/test_module_lightfuzz.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,143 @@ def check(self, module_test, events):
16911691
assert padding_oracle_detected, "Padding oracle vulnerability was not detected"
16921692

16931693

1694+
class Test_Lightfuzz_PaddingOracleDetection_Reflecting(Test_Lightfuzz_PaddingOracleDetection):
1695+
"""Padding oracle test where the server reflects the submitted value in the response body.
1696+
Without reflection-stripping logic, every probe body differs and detection always fails."""
1697+
1698+
def request_handler(self, request):
1699+
encrypted_value = quote(
1700+
"dplyorsu8VUriMW/8DqVDU6kRwL/FDk3Q+4GXVGZbo0CTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg=="
1701+
)
1702+
default_html_response = f"""
1703+
<html>
1704+
<body>
1705+
<form action="/decrypt" method="post">
1706+
<input type="hidden" name="encrypted_data" value="{encrypted_value}" />
1707+
<button type="submit">Decrypt</button>
1708+
</form>
1709+
</body>
1710+
</html>
1711+
"""
1712+
1713+
if "/decrypt" in request.url and request.method == "POST":
1714+
if request.form and request.form["encrypted_data"]:
1715+
encrypted_data = request.form["encrypted_data"]
1716+
if "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwAgLKWJi2nWKbh9ag5rnhm" in encrypted_data:
1717+
response_content = f"Padding error detected. Input: {encrypted_data}"
1718+
elif "4GXVGZbo0DTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg" in encrypted_data:
1719+
response_content = f"DIFFERENT CRYPTOGRAPHIC ERROR. Input: {encrypted_data}"
1720+
elif "AAAAAAA" in encrypted_data:
1721+
response_content = f"YET DIFFERENT CRYPTOGRAPHIC ERROR. Input: {encrypted_data}"
1722+
else:
1723+
response_content = f"Decryption failed. Input: {encrypted_data}"
1724+
1725+
return Response(response_content, status=200)
1726+
else:
1727+
return Response(default_html_response, status=200)
1728+
1729+
def check(self, module_test, events):
1730+
web_parameter_extracted = False
1731+
cryptographic_parameter_finding = False
1732+
padding_oracle_detected = False
1733+
for e in events:
1734+
if e.type == "WEB_PARAMETER":
1735+
if "HTTP Extracted Parameter [encrypted_data] (POST Form" in e.data["description"]:
1736+
web_parameter_extracted = True
1737+
if e.type == "FINDING":
1738+
if (
1739+
"Probable Cryptographic Parameter." in e.data["description"]
1740+
and "encrypted_data" in e.data["description"]
1741+
):
1742+
cryptographic_parameter_finding = True
1743+
1744+
if e.type == "VULNERABILITY":
1745+
if (
1746+
"Padding Oracle Vulnerability. Block size: [16]" in e.data["description"]
1747+
and "encrypted_data" in e.data["description"]
1748+
):
1749+
padding_oracle_detected = True
1750+
1751+
assert web_parameter_extracted, "Web parameter was not extracted"
1752+
assert cryptographic_parameter_finding, "Cryptographic parameter not detected"
1753+
assert padding_oracle_detected, "Padding oracle vulnerability was not detected"
1754+
1755+
1756+
class Test_Lightfuzz_PaddingOracleDetection_Noisy(Test_Lightfuzz_PaddingOracleDetection):
1757+
"""Padding oracle negative test: the server returns different responses for ~30 byte values,
1758+
which exceeds any valid block size. This should NOT produce a VULNERABILITY."""
1759+
1760+
def request_handler(self, request):
1761+
encrypted_value = quote(
1762+
"dplyorsu8VUriMW/8DqVDU6kRwL/FDk3Q+4GXVGZbo0CTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg=="
1763+
)
1764+
default_html_response = f"""
1765+
<html>
1766+
<body>
1767+
<form action="/decrypt" method="post">
1768+
<input type="hidden" name="encrypted_data" value="{encrypted_value}" />
1769+
<button type="submit">Decrypt</button>
1770+
</form>
1771+
</body>
1772+
</html>
1773+
"""
1774+
1775+
if "/decrypt" in request.url and request.method == "POST":
1776+
if request.form and request.form["encrypted_data"]:
1777+
encrypted_data = request.form["encrypted_data"]
1778+
# Check for the data block from the original ciphertext (mutate/truncate probes)
1779+
if "4GXVGZbo0DTh9YX1YvzZZJrYe4cHxvAICyliYtp1im4fWoOa54Zg" in encrypted_data:
1780+
response_content = "DIFFERENT CRYPTOGRAPHIC ERROR"
1781+
# Padding oracle probes: null IV + padding blocks produce long runs of A's in base64
1782+
elif encrypted_data.startswith("AAAAAAAAAAAAAAAA"):
1783+
try:
1784+
decoded = base64.b64decode(encrypted_data)
1785+
if len(decoded) >= 32:
1786+
varying_byte = decoded[31]
1787+
# 30 byte values produce a different response - way over any block size
1788+
if 100 <= varying_byte <= 129:
1789+
response_content = "Noisy error type A"
1790+
else:
1791+
response_content = "Decryption failed"
1792+
else:
1793+
response_content = "Decryption failed"
1794+
except Exception:
1795+
response_content = "Decryption failed"
1796+
# Arbitrary probe
1797+
elif "AAAAAAA" in encrypted_data:
1798+
response_content = "YET DIFFERENT CRYPTOGRAPHIC ERROR"
1799+
else:
1800+
response_content = "Decryption failed"
1801+
1802+
return Response(response_content, status=200)
1803+
else:
1804+
return Response(default_html_response, status=200)
1805+
1806+
def check(self, module_test, events):
1807+
web_parameter_extracted = False
1808+
cryptographic_parameter_finding = False
1809+
padding_oracle_detected = False
1810+
for e in events:
1811+
if e.type == "WEB_PARAMETER":
1812+
if "HTTP Extracted Parameter [encrypted_data] (POST Form" in e.data["description"]:
1813+
web_parameter_extracted = True
1814+
if e.type == "FINDING":
1815+
if (
1816+
"Probable Cryptographic Parameter." in e.data["description"]
1817+
and "encrypted_data" in e.data["description"]
1818+
):
1819+
cryptographic_parameter_finding = True
1820+
if e.type == "VULNERABILITY":
1821+
if "Padding Oracle" in e.data["description"]:
1822+
padding_oracle_detected = True
1823+
1824+
assert web_parameter_extracted, "Web parameter was not extracted"
1825+
assert cryptographic_parameter_finding, "Cryptographic parameter not detected"
1826+
assert not padding_oracle_detected, (
1827+
"Padding oracle should NOT be detected when 30 probes differ (exceeds block size)"
1828+
)
1829+
1830+
16941831
class Test_Lightfuzz_XSS_jsquotecontext(ModuleTestBase):
16951832
targets = ["http://127.0.0.1:8888"]
16961833
modules_overrides = ["httpx", "lightfuzz", "excavate", "paramminer_getparams"]
@@ -1911,3 +2048,15 @@ class Test_Lightfuzz_envelope_isolation_paddingoracle(Test_Lightfuzz_PaddingOrac
19112048
}
19122049
},
19132050
}
2051+
2052+
2053+
# Envelope state isolation: reflecting padding oracle detection with all submodules enabled.
2054+
class Test_Lightfuzz_envelope_isolation_paddingoracle_reflecting(Test_Lightfuzz_PaddingOracleDetection_Reflecting):
2055+
config_overrides = {
2056+
"interactsh_disable": True,
2057+
"modules": {
2058+
"lightfuzz": {
2059+
"enabled_submodules": ["sqli", "cmdi", "xss", "path", "ssti", "crypto", "serial", "esi"],
2060+
}
2061+
},
2062+
}

0 commit comments

Comments
 (0)