-
Notifications
You must be signed in to change notification settings - Fork 0
/
challenge_16.py
108 lines (78 loc) · 3.14 KB
/
challenge_16.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# CBC bitflipping attacks
# Without knowledge of the key, modify the ciphertext, so that it
# will decrypt to contain the string ';admin=true'.
# From Wikipedia:
#
# Note that a one-bit change to the ciphertext causes complete corruption of
# the corresponding block of plaintext, and inverts the corresponding bit in
# the following block of plaintext, but the rest of the blocks remain intact.
# This peculiarity is exploited in different padding oracle attacks, such as
# POODLE.
#
# So we start with
#
# |xxxxxxxxxxxxxxxx|x%3Badmin%3Dtrue|
# 0123456789012345 0123456789012345
#
# and want to get for instance
#
# |xxxxxxxxxxxxxxxx|xxxxx;admin=true|
# 0123456789012345 0123456789012345
# By manipulating bytes in the preceding cipher text block, the decryption
# of _that_ block will be scrambled (and we don't care about that), but
# corresponding bytes in the next block will change in the same way.
from os import urandom
import random
from urllib.parse import quote
from Crypto.Cipher import AES
from challenge_09 import pkcs7, strip_pkcs7
from challenge_15 import validate_pkcs7
random.seed(20240401)
# Random but fixed key
KEY = b"\xa1\x89\xf1/\xd4\x163b\xa5\xd6\x1f \xdc\xe8z\xda"
def prepare_payload(s: str) -> str:
#
# The essential thing in this function is that the payload will always
# be urllib quoted. The output will never contain the literal string
# 'admin=true'. But since there are no length restrictions on the
# payload an attacker can still directly craft a ciphertext that will
# decrypt to a string that contains 'admin=true'.
#
prefix = "comment1=cooking%20MCs;userdata="
suffix = ";comment2=%20like%20a%20pound%20of%20bacon"
return prefix + quote(s) + suffix
def encrypt_payload(s: str, key: bytes = KEY) -> tuple[bytes, bytes]:
plaintext = pkcs7(prepare_payload(s).encode("utf-8"))
iv = urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return cipher.encrypt(plaintext), iv
def decrypt_payload(s: bytes, key: bytes = KEY, iv: bytes = b"", rt=bytes) -> bytes:
cipher = AES.new(key, AES.MODE_CBC, iv=iv or urandom(16))
plaintext = cipher.decrypt(s)
if validate_pkcs7(plaintext):
plaintext = strip_pkcs7(plaintext)
return plaintext
def malicious_payload():
ciphertext, iv = encrypt_payload("x" * 16 + "x;admin=true")
cipherbytes = bytearray(ciphertext)
plaintext = decrypt_payload(ciphertext, iv=iv)
assert (
plaintext
== b"comment1=cooking%20MCs;userdata=xxxxxxxxxxxxxxxxx%3Badmin%3Dtrue;comment2=%20like%20a%20pound%20of%20bacon"
)
original = b"%3Badmin%3D"
modified = b"xxxx;admin="
offset = plaintext.find(original)
for i, (a, b) in enumerate(zip(original, modified)):
cipherbytes[offset + i - 16] ^= a ^ b
return bytes(cipherbytes), iv
if __name__ == "__main__":
payload, iv = malicious_payload()
plaintext = decrypt_payload(payload, iv=iv)
print("Plaintext:")
print(plaintext)
print()
if plaintext.find(b";admin=true;"): # type: ignore
print("Success! Plaintext contains ';admin=true;'.")
else:
print("Failure! Plaintext does not contain ';admin=true;'.")