[Cryptanalysis] AES - CBC - Bit-Flipping Attack

정확히 일주일 전에 문제를 보고 꽉 막혀있었는데 오늘 다시 보다가 풀이에 성공했네요 꺼이꺼이.. 쏴리질럿!!

I was struggling on this problem a week ago, but finally I succeed it.. hoooray!!

CBC Bit-Flipping 취약점을 이용한 문제라고 합니다. 해당 취약점을 모른다면 여기를 참고하세요.

This problem can be solved by CBC Bit-Flipping vulnerability. If you don't know, check here.

서비스에 접속하면 계정 생성/로그인/Leave가 있습니다. 계정 생성부터 먼저 합시다. 이름과 메일 주소를 아무렇게나 입력하고 나면 plaintext token과 AES CBC모드로 암호화됐음을 알려주고, 암호화된 토큰을 알려줍니다. 이래저래 실험을 해보면 저기 적힌 [id=546815648;name=testtest;is_member=false;mail=zcxvlzc;pad=00] 이 string을 암호화한 결과가 token이고, 블럭은 16바이트임을 알 수 있습니다.

When you enter the service, there are three menus. Let's create an account. When you enter the name and mail address as you want, server gives you plaintext token and encrypted token. With some experiments, the result of AES_CBC([id=546815648;name=testtest;is_member=false;mail=zcxvlzc;pad=00]) is token and block size is 16byte.

해당 토큰으로 로그인을 하면, is_member=false라 그런지 TEAM MEMBER ONLY라고 뜨고, 아마 is_member=true로 만들어야할 것 같습니다.

When I tried to login using given token, maybe since is_member=false, server returns "TEAM MEMBER ONLY". Maybe I should made is_member=true.

 

일단 임의로 토큰의 한 비트만 수정해서 전송을 해보았습니다.

I just modified one bit and sent to server.

 # 16byte 단위로 암호화함

import base64
token = 'IRZjBh6GxjeYI7YZvxwfBD1RaarKZvDEFU00ebc/9ADuZIXv5vk6QMoHInn4AaaJKF+x5/ZxcEyznNQElMgsqAUTgXV3k7vVK1K5471br0p/zdOI9yxwDEjeGugWdZZfoBwjLjsrN3r3NT4UiR/DUA=='
t = base64.b64decode(token) # len : 112
for i in t:
  print(hex(i),end= ' ')
tmp = bytearray(t)
tmp[37] ^= 0x01
print('\n',base64.b64encode(tmp))

당연히 복호화된 평문의 한 블럭이 깨지게되고 그로 인해 이상한 바이트가 출몰합니다. 그리고 Some weird char in that token이라고 하며 서비스를 죽입니다.

Obviously one block is broken and service was dead.

그 다음으로 이름 자체에 is_member=true; 를 실어보내어 어찌저찌 해보려고 했으나 이것 또한 필터링이 되었습니다. =과 ; 이 필터링되는 것으로 보입니다. 여기까지가 저번주의 일이었습니다.

Next, I tried to put "is_member=true;" on name but it was filtered. It seems "=" and ";" are filtered. This was what I tried in last week.

 

오늘은 맨 마지막 블럭을 깨보았는데, 놀랍게도 토큰이 정상적으로 실행됩니다. 뒤의 패딩 부분은 망가져도 상관이 없나봅니다. 근데 이거 솔직히 조금 게싱 아닌가여... 의도한게 이런거였으면 서비스 소스코드를 같이 주지..

Today, I break the last block, and service isn't die. It is little guessing, huh..? If it is intended, it is better to give source code of service.. :(

 

아무튼 그러면 이제 아래와 같은 시나리오를 생각해봅시다.

Anyway, let's consider below senario:

 

[id=546815648;name=01234567abcdefghABCDEFGH01234567abcdefghABCDEFGH;is_member=false;mail=test;pad=0000000000000]에 대응되는 토큰을 생각해봅시다. 토큰의 뒤에 a를 16개 덧댄 후 decrypt한 결과를 확인합니다.

Consider the token corresponds with [id=546815648;name=01234567abcdefghABCDEFGH01234567abcdefghABCDEFGH;is_member=false;mail=test;pad=0000000000000]. Put 'a'*16 on token and check the decrypted result.

 

그 결과의 마지막 블럭이 ;is_member=true] 가 되게끔 조작합니다.

And modify the token as last block represents ";is_member=true]".

# 16byte 단위로 암호화함

import base64
token = 'IRZjBh6GxjeYI7YZvxwfBD1RaarKZvDEFU00ebc/9ADuZIXv5vk6QMoHInn4AaaJKF+x5/ZxcEyznNQElMgsqAUTgXV3k7vVK1K5471br0p/zdOI9yxwDEjeGugWdZZfoBwjLjsrN3r3NT4UiR/DUA=='
t = base64.b64decode(token) # len : 112
for i in t:
  print(hex(i),end= ' ')
tmp = bytearray(t)
tmp[37] ^= 0x01 

tmp += bytearray('a'.encode()*16)
expected = bytearray(';is_member=true]'.encode())
current = bytearray(b'+\x10\xb1\r\x8c\xd3\x99\x05\x0f\xfa\xed\x0e\xdc\x94\xc8\x1b')
print(len(current),len(expected))
for i in range(16):
  tmp[96+i] ^= (expected[i]^current[i])

print('\n',base64.b64encode(tmp))

출력된 토큰이 과연 인증을 우회해줄지 두근두근하며 제출해보면..

When I submit the token...

키를 얻었습니다!

Gotcha!

  Comments