HackTheBox_Flippin_Bank

Dramatically solved.

HackTheBox_Flippin_Bank

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
import socketserver 
import socket, os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from Crypto.Random import get_random_bytes
from binascii import unhexlify
from secret import FLAG


wlcm_msg ='########################################################################\n'+\
'# Welcome to the Bank of the World #\n'+\
'# All connections are monitored and recorded #\n'+\
'# Disconnect IMMEDIATELY if you are not an authorized user! #\n'+\
'########################################################################\n'


key = get_random_bytes(16)
iv = get_random_bytes(16)


def encrypt_data(data):
padded = pad(data.encode(),16,style='pkcs7')
cipher = AES.new(key, AES.MODE_CBC,iv)
enc = cipher.encrypt(padded)
return enc.hex()

def decrypt_data(encryptedParams):
cipher = AES.new(key, AES.MODE_CBC,iv)
paddedParams = cipher.decrypt( unhexlify(encryptedParams))
print(paddedParams)
if b'admin&password=g0ld3n_b0y' in unpad(paddedParams,16,style='pkcs7'):
return 1
else:
return 0

def send_msg(s, msg):
enc = msg.encode()
s.send(enc)

def main(s):
send_msg(s, 'username: ')
user = s.recv(4096).decode().strip()

send_msg(s, user +"'s password: " )
passwd = s.recv(4096).decode().strip()

send_msg(s, wlcm_msg)

msg = 'logged_username=' + user +'&password=' + passwd

try:
assert('admin&password=g0ld3n_b0y' not in msg)
except AssertionError:
send_msg(s, 'You cannot login as an admin from an external IP.\nYour activity has been logged. Goodbye!\n')
raise

msg = 'logged_username=' + user +'&password=' + passwd
send_msg(s, "Leaked ciphertext: " + encrypt_data(msg)+'\n')
send_msg(s,"enter ciphertext: ")

enc_msg = s.recv(4096).decode().strip()

try:
check = decrypt_data(enc_msg)
except Exception as e:
send_msg(s, str(e) + '\n')
s.close()

if check:
send_msg(s, 'Logged in successfully!\nYour flag is: '+ FLAG)
s.close()
else:
send_msg(s, 'Please try again.')
s.close()


class TaskHandler(socketserver.BaseRequestHandler):
def handle(self):
main(self.request)

if __name__ == '__main__':
socketserver.ThreadingTCPServer.allow_reuse_address = True
server = socketserver.ThreadingTCPServer(('0.0.0.0', 1337), TaskHandler)
server.serve_forever()

CBC字节翻转?

一眼CBC字节翻转

看一半发现IV不可控

CBC密码块构造

想了一下可以构造伪密码块

CBC大概加解密流程如下

1
Plain_Block -> Xor(Last_Cipher_Block/IV) -> Block_Cipher_Encrypt -> Cipher_Block -> Block_Cipher_Decrypt -> Xor(Last_Cipher_Block/IV) -> Plain_Block

题目中的所需密码块如下

1
2
3
    Plain_Block_0       Plain_Block_1       Plain_Block_2
logged_username= admin&password=g 0ld3n_b0y
IV Cipher_Block_0 Cipher_Block_1 Cipher_Block_2

Key, IV, Plain_Block_0固定,则Cipher_Block_0固定

攻击思路如下

1
2
3
target = Plain_Block_1 ^ Cipher_Block_0
Cipher_Block_1 = encrypt(target)
Cipher_Block_2 = encrypt(Plain_Block_2 ^ Cipher_Block_1)

我们可以在Plain_Block_1Plain_Block_2中间插入Plain_Block_Payload
并且满足以下条件

1
2
Cipher_Block_Payload = encrypt(Plain_Block_Payload ^ Cipher_Block_1)
Cipher_Block_Payload == Cipher_Block_1

在这种情况下则有

1
2
3
Cipher_Block_Payload == Cipher_Block_1
Cipher_Block_Payload ^ Plain_Block_2 == Cipher_Block_1 ^ Plain_Block_2
encrypt(Cipher_Block_Payload ^ Plain_Block_2) == encrypt(Cipher_Block_1 ^ Plain_Block_2) == Cipher_Block_2

即我们可以获得Get Flag情况下Cipher_Block_2的内容

但是Plain_Block_Payload该如何构造?

Payload明文密码块构造

1
2
3
4
Cipher_Block_Payload == Cipher_Block_1
encrypt(Plain_Block_Payload ^ Cipher_Block_1) == encrypt(Plain_Block_1 ^ Cipher_Block_0)
Plain_Block_Payload ^ Cipher_Block_1 == Plain_Block_1 ^ Cipher_Block_0
Plain_Block_Payload == Plain_Block_1 ^ Cipher_Block_0 ^ Cipher_Block_1

所需的三个条件已知,所以我们可以很轻松的得到Plain_Block_Payload

Encode & Decode

将构造的内容用pwntools发出后没有收到响应

多次调试后发现问题所在

1
2
send_msg(s, user +"'s password: " )
passwd = s.recv(4096).decode().strip()

本地生成算出Payload Hex形式后转字符串是用ISO-8859-1编码,这个单字节编码在处理\x7f以上的字符时很好用,所以顺手就用这个编码去处理Payload了

但是问题随之而来

1
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa6 in position 15: invalid start byte

也就是意味着我的ISO-8859-1编码的Payload需要能够被UTF-8编码处理

且UTF-8编码之后长度不会变长, 否则在Pad时会出现问题

1
padded = pad(data.encode(),16,style='pkcs7')

WTF?

Guessing Game

所以现在所能做的,就只有去修改Plain_Block_1的内容

Plain_Block_1内容如下:

1
?????&password=?

爆破六位掩码,这会影响Cipher_Block_1的结果

由于AES加密过程的扩散性,我们可以将Cipher_Block_1的结果看成是随机的

我们所需要的结果是Payload_Block_Payload的16字节均小于\x80,这样UTF-8编码就能处理

概率为1/65536,但值得一试

我们再理一遍思路

原始密码块如下

1
2
3
    Plain_Block_0       Plain_Block_1       Plain_Block_2
logged_username= admin&password=g 0ld3n_b0y
IV Cipher_Block_0 Cipher_Block_1 Cipher_Block_2

我们需要的XOR结果如下

1
target = Cipher_Block_0 ^ Plain_Block_1

现在我们开始爆破Plain_Block_1,可以得到Tmp_Cipher_Block_1

1
2
3
    Plain_Block_0       Tmp_Plain_Block_1   Tmp_Plain_Block_Payload     Plain_Block_2
logged_username= ?????&password=? ???????????????? 0ld3n_b0y
IV Cipher_Block_0 Tmp_Cipher_Block_1 Tmp_Cipher_Block_Payload Cipher_Block_2

得到的Tmp_Cipher_Block_1视为随机生成,可以轻松得到Tmp_Plain_Block_Payload

1
Tmp_Plain_Block_Payload = Tmp_Cipher_Block_1 ^ target

我们所需要确保的,就是这个Tmp_Plain_Block_Payload可以被UTF-8编码处理

在爆破出Tmp_Plain_Block_Payload之后,由于Tmp_Cipher_Block_PayloadCipher_Block_1相等,Cipher_Block_2为预期值

输入Cipher_Block_0 + Tmp_Cipher_Block_Payload + Cipher_Block_2即可获得Flag

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
from pwn import *
import binascii
import os

host = "143.110.169.131"
port = 30993
p = remote(host, port)
data = p.recv()
p.sendline(b"admin")
data = p.recv()
payload_block = "0123456789abcdef"
p.sendline(("g" + payload_block + "0ld3n_b0y").encode())
data = p.recvuntil("Leaked ciphertext: ")
data = p.recv()
data = data.decode().replace("\nenter ciphertext: ", "")
p.close()

cipher_0 = data[:32]
plain_1 = "admin&password=g"
target = int(cipher_0, 16) ^ int((binascii.hexlify(plain_1.encode()).decode()), 16)
count = 0

while True:
count += 1
print(count)

p = remote(host, port)
data = p.recv()

username = ""
for _ in range(5):
username = username + chr(ord(os.urandom(1).decode("ISO-8859-1")) % 128)
#print(username)

p.sendline(username.encode())
data = p.recv()
payload_block = "0123456789abcdef"
pass_chr = chr(ord(os.urandom(1).decode("ISO-8859-1")) % 128)
#print(pass_chr)

p.sendline((pass_chr + payload_block + "0ld3n_b0y").encode())
data = p.recvuntil("Leaked ciphertext: ")
data = p.recv()
data = data.decode().replace("\nenter ciphertext: ", "")
p.close()

cipher_1 = data[32:64]
ready = 1

payload_hex = hex(target ^ int(cipher_1, 16))[2:]
if len(payload_hex) % 2 != 0:
payload_hex = "0" + payload_hex
payload_block = binascii.unhexlify(payload_hex.encode()).decode("ISO-8859-1")
#print(payload_hex)

for _ in payload_block:
if ord(_) not in range(0, 128):
#print(hex(ord(_))[2:])
ready = 0
break
if ready == 1:
#print(payload_block)
#print(binascii.hexlify(username.encode()).decode())
#print(binascii.hexlify(pass_chr.encode()).decode())
#print(binascii.hexlify(payload_block.encode()).decode())
break

p = remote(host, port)
data = p.recv()
p.sendline(username.encode())
data = p.recv()
p.sendline((pass_chr + payload_block + "0ld3n_b0y").encode("ISO-8859-1"))
data = p.recvuntil("Leaked ciphertext: ")
data = p.recv()
data = data.decode().replace("\nenter ciphertext: ", "")
payload = data[:32] + data[64:128]
p.sendline(payload.encode())
data = p.recv()
p.close()
print(data)

😅

不需要验证前面的"logged_username="是吧

直接字节翻转就完事了

密码块如下

1
logged_username=    Admin&password=g    0ld3n_b0y
1
2
decrypt(Block_Cipher_1)[0] == "A" ^ Block_Cipher_0[0]
New_Block_Cipher_0[0] = "A" ^ "a" ^ Block_Cipher_0[0]

代入得到

1
decrypt(Block_Cipher_1)[0] == "a"

难怪做的有点费劲,原来是非预期了啊😅,感觉这个思路都能出个题了都