Automne's Shadow.

RCTF 2019 BabyCrypto WriteUp

2019/07/01 Share

Crypto

题目给出了源码

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
#!/usr/bin/python3 -u
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from hashlib import sha1
import binascii
import json
import os
import re
import sys

def pad(s):
padder = padding.PKCS7(128).padder()
return padder.update(s) + padder.finalize()

def unpad(s):
unpadder = padding.PKCS7(128).unpadder()
return unpadder.update(s) + unpadder.finalize()

key = os.urandom(16)
iv = os.urandom(16)
salt = key

iv_len = 16

print("Input username:")
username = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", username):
print("Invalid username")
exit()

print("Input password:")
password = sys.stdin.readline().strip()
if not re.match("^[a-z]{5,10}$", password):
print("Invalid password")
exit()

cookie = b"admin:0;username:%s;password:%s" %(username.encode(), password.encode())

h = sha1()
h.update(salt)
h.update(cookie)
hv = h.digest()
hv_hex = h.hexdigest()
hash_len = len(hv)

cookie_padded = pad(cookie)

backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
cookie_padded_encrypted = encryptor.update(cookie_padded) + encryptor.finalize()
print("Your cookie:")
print(binascii.hexlify(iv).decode() + binascii.hexlify(cookie_padded_encrypted).decode() + hv_hex)

def is_valid_hash(cookie, hv):
h = sha1()
h.update(salt)
h.update(cookie)
return hv == h.digest()

while True:
try:
print("Input your cookie:")
data_hex = sys.stdin.readline().strip()
data = binascii.unhexlify(data_hex)
assert(len(data) > iv_len + hash_len)
iv, cookie_padded_encrypted, hv = data[:iv_len], data[iv_len: -hash_len], data[-hash_len:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
cookie_padded = decryptor.update(cookie_padded_encrypted) + decryptor.finalize()
try:
cookie = unpad(cookie_padded)
except Exception as e:
print("Invalid padding")
continue
if not is_valid_hash(cookie, hv):
print("Invalid hash")
continue
info = {}
for _ in cookie.split(b";"):
k, v = _.split(b":")
info[k] = v
if info[b"admin"] == b"1":
with open("flag") as f:
flag = f.read()
print("Your flag: %s" %flag)
else:
print("Goodbye nobody")
exit()
except Exception as e:
print("Invalid cookie %s" %e)

先在本地把环境搭起来,使用socat

1
socat -T60 TCP-LISTEN:8000,reuseaddr,fork EXEC:"python -u crypto.py"

然后连接测试

automne

打印出的cookie内容从下面的代码里可以看到由3部分组成:iv + padded(cookie) + mac

1
2
print("Your cookie:")
print(binascii.hexlify(iv).decode() + binascii.hexlify(cookie_padded_encrypted).decode() + hv_hex)

做个测试

1
2
3
4
5
6
7
8
9
10
11
12
Python 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import binascii
>>> iv = 'aaaaaaaaaaaaaaaa'
>>> print binascii.hexlify(iv)
61616161616161616161616161616161
>>> print binascii.hexlify(iv).decode()
61616161616161616161616161616161
>>> print len(iv)
16
>>> print len(binascii.hexlify(iv).decode())
32

所以说打印出的binascii.hexlify(iv).decode()长度就是32字节(因为iv长度为16字节)

接着看binascii.hexlify(cookie_padded_encrypted).decode() 的长度

cookie_padded_encrypted就是将cookie_padded作为明文进行AES-CBC-128加密后的密文,密文长度与明文长度一致,明文cookie_padded对cookie进行了填充,看看cookie的内容

1
cookie = b"admin:0;username:%s;password:%s" %(username.encode(), password.encode())

因为username和password的长度限制在{5,10}之间,也就是把明文的长度限制到3个分组(3*16=48字节)

假设用户名密码均为admin,那么cookie明文如下:

1
admin:0;username:admin;password:admin

也就是说binascii.hexlify(cookie_padded_encrypted).decode() 的长度为48*2=96字节。

剩下的40字节长度就是hv_hex的长度了。

当然这些长度也可以直接修改源码打印出来。

接着看代码中获取flag的关键逻辑

1
2
3
4
5
6
7
8
info = {}
for _ in cookie.split(b";"):
k, v = _.split(b":")
info[k] = v
if info[b"admin"] == b"1":
with open("flag") as f:
flag = f.read()
print("Your flag: %s" %flag)

可以看到要求cookie中的admin:1,但是cookie的最前面已经写死了admin:0,这一点可以使用下面的方式绕过

1
admin:0;username:admin;password:admin;admin:1

但是username和password的形式限制只允许输入字母

梳理思路:

代码中的原iv可知,且iv可控,且会打印出"Invalid padding"错误,因此可以使用CBC Padding Oracle攻击来修改明文(cookie)为admin:0;username:admin;password:admin;admin:1

修改了iv和明文,还要继续修改mac消息验证码才不会报错,鉴于代码中使用的是sha1算法,所以可以使用哈希长度拓展攻击来绕过

参考官方writeup里的脚本

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
#!/usr/bin/env python
# CBC padding attacks + Length extension attacks

from cryptography.hazmat.primitives import padding
from pwn import *
import hashpumpy

block_size = 16
salt_len = 16
username = "admin"
password = "admin"
ori_cookie = b"admin:0;username:%s;password:%s" %(username, password)
extra_data = ";admin:1"

def pad(s):
padder = padding.PKCS7(block_size*8).padder()
return padder.update(s) + padder.finalize()

io = remote('192.168.126.146',8000)
io.readuntil("Input username:\n")
io.writeline(username)
io.readuntil("Input password:\n")
io.writeline(password)
io.readuntil("Your cookie:\n")
ori_hash = io.readline().strip()[-40:]

new_hash, new_cookie = hashpumpy.hashpump(ori_hash, ori_cookie, extra_data, salt_len)
print len(new_cookie)
target_padded = pad(new_cookie)
print len(target_padded)

def is_valid_pad(iv, cipher):
io.readuntil("Input your cookie:\n")
data = enhex(str(iv)) + enhex(str(cipher)) + new_hash
io.writeline(data)
data = io.readline()
return "Invalid padding" not in data

def gen_iv(cipher, target):
assert(len(cipher)==block_size)
assert(len(target)==block_size)
iv = bytearray(block_size)
mid = bytearray(block_size)
for i in range(1, block_size+1):
print(i)
for j in range(1, i):
iv[-j] = mid[-j] ^ i
for j in range(256):
iv[-i] = j
if is_valid_pad(iv, cipher):
mid[-i] = iv[-i] ^ i
break
if j==255:
exit()
return xor(mid, target)

data = bytearray(16)
result = enhex(str(data))

for i in range(len(target_padded)//block_size-1, -1, -1):
iv = gen_iv(data, target_padded[i*block_size: (i+1)*block_size])
print iv
result = enhex(iv) + result
data = bytearray(iv)

io.readuntil("Input your cookie:\n")
data = result + new_hash
io.writeline(data)
io.interactive()

本地运行代码即可

automne

更新于2019.7.14

上面的代码简化标注后如下,便于理解:

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
from cryptography.hazmat.primitives import padding
from pwn import *
import hashpumpy

username = "admin"
password = "admin"
ori_cookie = b"admin:0;username:%s;password:%s" %(username, password)
extra_data = ";admin:1"

def pad(s):
padder = padding.PKCS7(16*8).padder()
return padder.update(s) + padder.finalize()

io = remote('192.168.126.146',8000)
io.readuntil("Input username:\n")
io.writeline(username)
io.readuntil("Input password:\n")
io.writeline(password)
io.readuntil("Your cookie:\n")
ori_hash = io.readline().strip()[-40:]

new_hash, new_cookie = hashpumpy.hashpump(ori_hash, ori_cookie, extra_data, 16)
print len(new_cookie)
target_padded = pad(new_cookie)
print len(target_padded)

def is_valid_pad(iv, cipher):
io.readuntil("Input your cookie:\n")
data = enhex(str(iv)) + enhex(str(cipher)) + new_hash
io.writeline(data)
data = io.readline()
#print data
return "Invalid padding" not in data

def gen_iv(cipher, target):
iv = bytearray(16) #\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
mid = bytearray(16)
print iv
for i in range(1, 17):
print(i) #1
for j in range(1, i):
iv[-j] = mid[-j] ^ i #iv[-1] = mid[-1] ^ 1
for j in range(256):
iv[-i] = j #iv[-1] = 0
if is_valid_pad(iv, cipher):
print j
print iv
mid[-i] = iv[-i] ^ i #mid[-16] = iv[-16]^16
break
if j==255:
exit()
print mid
print len(mid) #16
return xor(mid, target)

data = bytearray(16)
result = enhex(str(data))

for i in range(3, -1, -1): #3,2,1,0
print i
print data
iv = gen_iv(data, target_padded[i*16: (i+1)*16])
result = enhex(iv) + result
data = bytearray(iv)

io.readuntil("Input your cookie:\n")
data = result + new_hash
io.writeline(data)
io.interactive()
CATALOG