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, osfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad,unpadfrom Crypto.Random import get_random_bytesfrom binascii import unhexlifyfrom secret import FLAGwlcm_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_1
与Plain_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
内容如下:
爆破六位掩码,这会影响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_Payload
与Cipher_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 binasciiimport oshost = "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 ) p.sendline(username.encode()) data = p.recv() payload_block = "0123456789abcdef" pass_chr = chr (ord (os.urandom(1 ).decode("ISO-8859-1" )) % 128 ) 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" ) for _ in payload_block: if ord (_) not in range (0 , 128 ): ready = 0 break if ready == 1 : 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"
难怪做的有点费劲,原来是非预期了啊😅,感觉这个思路都能出个题了都