在做ctf题目的过程中,遇到了cbc字节翻转攻击的利用技巧,在深入学习之后,觉得应该记录下来,以免遗忘。
AES CBC模式的加密与解密原理
分组密码链接模式的特点在于:加密时,每一个明文分组(除了第一个明文分组)加密之前都需要和前一个密文分组进行异或处理之后,才可以进行加密处理;解密时,每一个密文分组经过解密处理之后,都需要和前一个密文分组进行异或处理,才可以得到对应的明文分组。
分组密码链接模式,顾名思义,加密和解密过程都是以分组进行的。每一个分组大小为128bits(16字节),如果明文的长度不是16字节的整数倍,需要对最后一个分组进行填充(padding),使得最后一个分组长度为16字节。
对于加密时的第一个明文分组,需要通过和IV(初始化向量)进行异或处理之后,才可以进行加密处理;解密时的第一个密文分组,解密之后,需要通过和IV进行异或处理,才可以得到第一个明文分组。
这里的IV为不可预测的,随机生成的16字节向量,它不需要保密,但是需要保证完整性。
CBC模式的加解密过程
- 加密过程
- 1.将明文的第一个分组与IV进行异或,送入加密模块进行加密,得到第一个密文分组。
- 2.从第二个明文分组开始,将明文分组与前一个密文分组进行异或。
- 3.将第2步得到的结果送入加密模块进行加密。
- 4.将每一个密文分组拼接起来形成密文。
假设明文分组的下标从1开始
C0=IV
Ci=Ek(Pi⊕Ci−1)
- 解密过程
- 1.将密文的第一个分组进行解密,得到的结果与IV进行异或处理,得到第一个明文分组。
- 2.从第二个密文分组开始,先对每一个密文分组进行解密处理,到第3步。
- 3.将第2步得到的结果与前一个密文分组进行异或处理,得到对应的明文分组。
- 4.将每一个明文分组拼接在一块,便得到原先的明文。
Pi=Dk(Ci)⊕Ci−1
C0=IV
从上面解密过程中,我们可以发现,解密明文分组的过程受前一个密文分组的影响,所以我们可以通过控制前一个密文分组的内容,进而控制解密明文的内容。
CBC翻转攻击的原理
从上面的分析我们可以看出来,CBC字节翻转攻击发生在解密的时候。
假设我们只考虑单字节的操作。
Pi[0]=Dk(Ci)[0]⊕Ci−1[0]
0 = Pi[0]⊕Dk(Ci)[0]⊕Ci−1[0]
Pnew=Pi[0]⊕Dk(Ci)[0]⊕Ci−1[0]⊕Pnew
这里Pnew为我们想要的明文。
通过上面的操作,我们可以看到,如果我们让前一个密文分组对应的字节的值,修改为Ci−1[0]⊕Pi[0]⊕Pnew,就可以达到修改明文的目的。通过这种方法,便可以绕过服务器的检测。
利用实例
接下来,举一个例子,来说明具体如何利用这种技巧。
首先将泄露的代码拷贝下来,进行审计。
1 | define("SECRET_KEY", '***********'); |
通过对代码的分析,可以看出它的处理逻辑。
1.首先判断post提交的数据当中是否包含id值;如果包含,转到第2步;如果没有包含,转到第3步。
2.将id的值读取出来,并进行sql注入检测,在sqliCheck函数当中,对常见的特殊字符都进行了过滤。然后对id的值进行了aes-cbc加密处理,并将IV和加密后的内容cipher,作为cookie一并返回。
3.如果post提交的内容当中,不包含id,则将post当中的IV和cipher提取出来,在show_homepage函数当中进行操作。
仔细分析关键代码,发现有两个地方存在问题:
1.show_homepage函数对IV和cipher传过来的值并没有进行校验,是一个绕过的点。
2.sqliCheck函数的过滤,并不完善,可以通过%00截断后面的数据。来构造sql注入语句,select * from users limit 1。因为原先的sql语句limit的rows参数为0,所以无论id值为多少,都不会将username的值echo出来。
需要两步操作,来完成输出username的值。
1.post提交id的值为1;%00。
2.将返回的IV和cipher放在cookie域当中,并去掉id值。提交。
然而username里面放的值不是我们想要的。现在我们需要用到cbc字节翻转,来构造sql注入语句。
由于sqliCheck函数将‘=’,‘,’,‘union'
全部过滤了,所以需要用1nion代替union,用join代替逗号,用regexp代替等号,然后使用cbc字节翻转将1换为u,得到union。
为了方便,写一个脚本,进行测试。
1 | import requests |
这里需要注意的是,当我们修改密文的第一个分组,来使得第二个明文分组更改为我们想要的结果,会破坏原先的第一个明文分组的内容,导致show_homepage()函数进行反序列化处理的时候发生异常,所以我们需要对IV进行处理,使得第一个明文分组恢复为原先的值。
通过sql注入测试,知道了数据库中有两个表:user和you_want。显然,you_want表当中包含我们需要的东西,进一步注入得到you_want表中只有一个字段value。
所以我们构造如下的payload:
'id':'0 2nion select * from((select 1)a join (select * from you_want)b join (select 3)c);'+chr(0)
结果如下: