首先padding oracle的文章真的很多,但是我感觉到奇怪的是我在中英文搜索引擎都没有找到Encypted Pastebin的通关方法(如果你找到了可以评论给我,我要看看是谁跟我一样这么喜欢写日记),是因为太简单吗,但是我看到官方在twitter上面在去年2月的时候说有一千多人拿到了前两题的flag,但是只有37个黑客通关,所以我觉着可能还是大部分人没有深刻理解其中的细节,padding oracle说实话原理没什么难的,难的在于你清楚各种变形情况下的解法,用此篇writeup当作我进入安全圈的第一篇文章
先给出我的通关证据:
padding oracle依据就是两个特性:
1. XOR的特性:交互律,结合律,还注意一个特性:0001 XOR 0001 = 0000 即同一个值可互相抵消
2. padding规则:编码的区块长度是L,则明文P的padding是b个值为b的字节,且b属于[1,L],注意到b不等于0
以PKCS#5举例,L=8,则b属于[1,8]
明文是:LYHISTORY
Padding结果为:L|Y|H|I|S|T|O|R|Y|07|07|07|07|07|07|07 (不包含竖线,是为了看得清晰)
如果padding不符合规则,通常服务器会抛出padding相关的错误信息,当然代码作者可以对信息进行一定程度的模糊处理,所以有时候要对这个错误信息进行斗智斗勇,这个细节需要注意,当然我们此处的CTF题目并没有增加这个难度点;
#0X01 Flag 0: 验证存在padding oracle攻击点
随便填写点东西提交,得到:
http://XXXXXXXXXXXXXXX/?post=GOdGAowxThxkJHSU0yCTcnJmaPuMKIoj-J3YB382zWJcCxqUi80KtmS4CMsrHGSs-MbZYqzeja1H9lC06YjRQokhLDCKXDDR1mo!gU5EmETXPx6AZYaGAHU2dxYJzXYR52p!y6xPaGGKQuwTJJ7uy-uZBgntw45qjsXxlWnAMd09N9Wr8KOKzFHMOAm4FKzSNzeAWivqtGdRbM2ksu2sIw~~
首先观察下,看起来像base64编码,但是后面一般默认==结尾,应该是url safe的一种处理;
验证padding oracle的存在简单来说,可以做两步:
Step 1.改变第一个字节:
通常对于加密数据,不管是否带iv,改变第一个字节,可以引发语法检查错误,我们改变G为H,得到
“`
^FLAG^*******************************************$FLAG$
Traceback (most recent call last):
File “./main.py”, line 69, in index
post = json.loads(decryptLink(postCt).decode(‘utf8’))
File “/usr/local/lib/python2.7/json/__init__.py”, line 339, in loads
return _default_decoder.decode(s)
File “/usr/local/lib/python2.7/json/decoder.py”, line 364, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File “/usr/local/lib/python2.7/json/decoder.py”, line 382, in raw_decode
raise ValueError(“No JSON object could be decoded”)
ValueError: No JSON object could be decoded
“`
第一个flag大家都看到了,这里又有一个细节:如果是iv,解密会失败,如果不带iv(服务器使用静态iv),解密的第一个区块会乱码,当然这里我们并不能确定,因为可能是解密失败造成非法json,也可能是我们刚好改了json的结构造成json非法,这个不是非常重要,我们可以先假设第一个区块是iv,如果不是,根据我们后面的原理,我们会知道第一个区块的明文是无法破解的,一般情况下我们如果知道明文和对应的密文,可以通过中间值算出iv,但是这里的ctf有点特殊,总之我们可以先假设第一个区块是iv,看看能破解出来多少东西就会知道了
Step 2.改变最后一个字节,会引发padding错误:
我这里先做一个错误的示范,可能很多人会这么以为:
http://XXXXXXXXXXXXX/?post=GOdGAowxThxkJHSU0yCTcnJmaPuMKIoj-J3YB382zWJcCxqUi80KtmS4CMsrHGSs-MbZYqzeja1H9lC06YjRQokhLDCKXDDR1mo!gU5EmETXPx6AZYaGAHU2dxYJzXYR52p!y6xPaGGKQuwTJJ7uy-uZBgntw45qjsXxlWnAMd09N9Wr8KOKzFHMOAm4FKzSNzeAWivqtGdRbM2ksu2sIw~A
就是最后一个字节嘛,将~改成比如A不就行了,你会得到什么呢:
“`
^FLAG^^*******************************************$$FLAG$
Traceback (most recent call last):
File “./main.py”, line 69, in index
post = json.loads(decryptLink(postCt).decode(‘utf8’))
File “./common.py”, line 46, in decryptLink
data = b64d(data)
File “./common.py”, line 11, in
b64d = lambda x: base64.decodestring(x.replace(‘~’, ‘=’).replace(‘!’, ‘/’).replace(‘-‘, ‘+’))
File “/usr/local/lib/python2.7/base64.py”, line 328, in decodestring
return binascii.a2b_base64(s)
Error: Incorrect padding
“`
怎么感觉不对,又是一个细节,这里抛错的incorrect padding跟padding oracle无关,因为这是base64函数抛出的,我来简单讲解下这里的知识点:
这里显示的是ascii码,一个ascii是一个字节,但是因为是base64编码,所以这里的一个ascii码表示的是base64的6个位所代表的一个“显示ascii码”(一个可打印字符),而实际上base64不是一个个6位去对应的(是24位对应4个字符),还有以下规则:
base64: 2^6=64 6位表示一个base64编码,标准Base64只有64个字符;
Base64是把3个字节变成4个可打印字符(3×8=4×6,为什么这样?因为6和8的最大公约数是24,所以要让6位的base64系统跟8位的bytes系统切换,就要进行补足),所以Base64编码后的字符串一定能被4整除(不算用作后缀的等号);
等号一定用作后缀,且数目一定是0个、1个或2个。 这是因为如果原文长度不能被3整除,Base64要在后面添加 凑齐3n位
所以我并没有达到第二步的目的,正确的做法应该是比如将末尾的`Iw~~`改成比如`AA~~`
`http://XXXXXXXXXX/?post=GOdGAowxThxkJHSU0yCTcnJmaPuMKIoj-J3YB382zWJcCxqUi80KtmS4CMsrHGSs-MbZYqzeja1H9lC06YjRQokhLDCKXDDR1mo!gU5EmETXPx6AZYaGAHU2dxYJzXYR52p!y6xPaGGKQuwTJJ7uy-uZBgntw45qjsXxlWnAMd09N9Wr8KOKzFHMOAm4FKzSNzeAWivqtGdRbM2ksu2sAA~~`
由此得到:
“`
Traceback (most recent call last):
File “./main.py”, line 69, in index
post = json.loads(decryptLink(postCt).decode(‘utf8’))
File “./common.py”, line 49, in decryptLink
return unpad(cipher.decrypt(data))
File “./common.py”, line 22, in unpad
raise PaddingException()
PaddingException
“`
这里才是我们想要的PaddingException,当然我们也看到了unpad(cipher.decrypt(data)),佐证了我们的想法
#0X02 Flag 1: padding oracle解密攻击
好了,有了上面的验证,我们就知道可以玩padding oracle了,你自己写工具也好,用padbuster也好,一定要了解其原理,不然就会跟大多数人一样,拿不到后面两个flag,所以这里必须进行原理铺垫;
原理有好几种不同的思考方式,但是有些方式仅仅有助于解密攻击理解,但是不利于后面的加密攻击理解,所以我今天就只讲其中一个思路;
主要用到的公式:
Ci = Ek(Pi ⊕ Ci-1), Pi = Dk(Ci) ⊕ Ci-1, and C0 = IV; c={C0,C1…Ci…}, p={P1,P2…Pi…}
解释:
Ek是加密算法,Dk是解密算法,k是密钥,Ci代表第i个block对应的密文,Pi代表第i个block的明文或对应的编码,至于为什么C0=IV也就是引入一个随机变量呢,我们可以想象如果C0直接用Ek(P0),即用第一个block的明文加密的话,那么每个以P0开头的明文加密结果都是以C0开头,相信你会觉着跟md5的彩虹表攻击类似,所以这样很不好,所以需要IV就相当于这里的“盐”,集-合c是全部密文(当然有的可能不带C0),集-合p是全部明文或编码
我们先来举例一个最简单的场景,c={C0,C1} p={P1},就是只有一个明文区块的情况:
C1 = Ek(P1 ⊕ C0)
P1 = Dk(C1) ⊕ C0 = Dk(C1) ⊕ IV
我们这里攻击时,每次只处理一个block的一个字节,怎么玩呢:
last-byte-of(P1)
= last-byte-of(Dk(C1) ⊕ C0)
= last-byte-of(Dk(C1) ⊕ IV)
我们让last-byte-of(C0′)=last-byte-of(IV’)=0x0G,G代表guess猜测的字节,其他字节全部置为0, G从0x00到0xff 256中可能,
换句话C0’=IV’属于[0x00000…00, 0x00000…ff]
我们现在开始猜测:
P1′
= Dk(C1) ⊕ C0′
= Dk(C1) ⊕ IV’
= Dk(C1) ⊕ 0x0G
= P1 ⊕ 0x0G
这里将Dk(C1)的结果我们称为中间值,然后我们操作的字节,比如这里的最后一个字节称为中间值字节,
假设P1⊕0x0G的结果是:0x01,
根据padding规则,
0x01
0x0202
0x030303
都是合法的padding,发送C0’|C1到服务可以通过padding验证(就是对应前面第一小节我们得到的PaddingException,服务器会根据C0’和C1 算P1′ = Dk(C1) ⊕ C0’,如果是合法的padding,我们就可以推导出中间值字节:
Dk(C1) ⊕ 0x0G = 0x01
=>
last-byte-of(Dk(C1)) = 0x0G ⊕ 0x01
我们得到了中间值,再异或上真正的IV的第一个字节就得出了明文区块P1的最后一个字节,即
last-byte-of(P1)
= last-byte-of(Dk(C1) ⊕ IV)
= 0x0G ⊕ 0x01 ⊕ last-byte-of(IV)
至此我们利用PaddingException作为判断条件从而推导出明文区块P1的最后一个字节
再拓展一下,我们可以任意抽取C(i-1)|C(i),构造C(i-1)’,从第一个字节遍历256种可能找到中间值,然后再XOR C(i-1)的第一个字节获取对应的plain text
接着前面得到的第一个字节,我们通过Padding 0x0202可以拿到P1的第二个字节,依次类推….
直到P1的第一个字节:
如果区块大小是8个字节,最后的padding条件是 0x0808080808080808
如果区块大小是16个字节,最后的padding条件是 0x10101010101010101010101010101010
我简单画了一个图解,大家可以将上面的公式结合这个图来脑补一下整个解密过程:
你可以自己写工具或者使用padbuster,padbuster用法
padbuster http://XXXX/?post=
16代表区块长度,0代表默认的编码 0=Base64, 1=Lower HEX, 2=Upper HEX 3=.NET UrlToken, 4=WebSafe Base64
需要注意的是,前面的编码跟这里的websafe base64并不完全一致,需要我们自己按照前面报错提示替换成正常的base64:
`replace(‘~’, ‘=’).replace(‘!’, ‘/’).replace(‘-‘, ‘+’)`
结果拿到第二个flag
“`
** Finished ***
[+] Decrypted value (ASCII): {“flag”: “^FLAG^××××××××××××××××××××××××××××××$FLAG$”, “id”: “3”, “key”: “3R7pl-uUjx-!COpjFc1jlA~~”}
“`
#0X03 Flag 2: padding oracle加密攻击
观察前面的flag结果,我们猜测这个密文是构造了几个参数,然后发送到server,获取到了我们创建的信息,相信很多人会对id感兴趣,我们很自然的好奇id=1对应的是啥,我们有没有办法去query id=1的结果呢,换句话,我们如何对
{“id”: “1”} 进行加密,我们先不管flag和key
现在再来理解下这个公式
Pi = Dk(Ci) ⊕ Ci-1, and C0 = IV; c={C0,C1…Ci…}, p={P1,P2…Pi…}
配上如下图解
大家可以比较直观的看到,我例子用的数据就是第一个区块的密文,i=1,所以Ci-1就是C0也就是IV,
我们现在思考一下:
令
P1={“id”: “1”},因为是11个字节,需要补全变成
P1′ = {|”|i|d|”|:| |”|1|”|}|05|05|05|05|05
我们需要利用的就是现在已知的是中间值:
C0′ = 中间值⊕P1′
从而获得新的C0’也就是新IV’,
从而我们完成了对P1’的加密,对应的新的加密密文即 c={C0′, C1}
看起来我们已经达到目的了,发送服务器,会得到如下结果
“`
Attempting to decrypt page with title: ^FLAG^*************************************************$FLAG$
Traceback (most recent call last):
File “./main.py”, line 74, in index
body = decryptPayload(post[‘key’], body)
KeyError: ‘key’
“`
从而拿到第三个flag
如果你不想手算,可以用padbuster,用法:
padbuster http://XXXX/?post=
#0X04 Flag 3: padding oracle加密攻击+sql注入
既然改变id可以获取到不同的东西,何不尝试下是否存在sql注入,相信sql注入不需要我多说了,其实跟前面一个flag没啥区别,但是我这里需要讲一个重要的东西,就是跟前面一个flag略微不同的地方,记得前面一小节我们说P1′ 刚好没有超过16个bits,所以构造一个区块就能搞定加密,但是我们这里进行sql注入很容易超过16个bits,比如假设整个明文是两个区块大小怎么搞?
当然你继续用padbuster,工具会帮你搞定,但是理解原理才是真正深刻的掌握,这个地方略微烧脑,假设新的明文p={P1′, P2′}
通过上一节我们已经知道了P1和P2对应的中间值我们是可以利用的,我们倒过来处理,我们先通过
C1′ = C2中间值⊕P2′
从而获取到第一个区块的加密结果,这个跟前面没有区别,重点来了,再来看第一个区块:
C0′ = C1中间值⊕P1’,发现问题了,C1的中间值不可以用了,为什么?因为为了让第二个区块有效,第一个区块的密文必须是C1’而不可以是C1,所以自然也不能用C1的中间值,怎么办?
其实很简单,这里需要再将脑回路绕到前面的Flag 1,还记得我们用解密攻击可以获取中间值的过程吗,我们这里可以将C1’作为密文通过前面的方法获取其对应的中间值C1’中间值,从而:
C0”=C1’中间值⊕P1′
最终我们获得了两个区块对应的加密密文 c={C0”, C1′, C2}
依次类推,我们可以获取任意个区块的密文
具体sql注入过程我就不说了,留一点点乐趣给没有通关的兄弟们,最终可以拿到如下的结果:
“`
Attempting to decrypt page with title: Referer: http://XXXX/?post=YYYYYYYYYYYYYYYYYYYY
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36
Connection: close
Host: 127.0.0.1:14807
Accept: image/webp,image/apng,image/*,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
“`
然后提取下referer的数据发送到服务器就可以解密出最终的flag
我这篇文章发布之后,hacker101这一关成功的人数会不会突然增加…..
请登录后发表评论
注册