Hacker101 Encrypted Pastebin 通关教科书式详解

首先padding oracle的文章真的很多,但是我感觉到奇怪的是我在中英文搜索引擎都没有找到Encypted Pastebin的通关方法(如果你找到了可以评论给我,我要看看是谁跟我一样这么喜欢写日记),是因为太简单吗,但是我看到官方在twitter上面在去年2月的时候说有一千多人拿到了前两题的flag,但是只有37个黑客通关,所以我觉着可能还是大部分人没有深刻理解其中的细节,padding oracle说实话原理没什么难的,难的在于你清楚各种变形情况下的解法,用此篇writeup当作我进入安全圈的第一篇文章

先给出我的通关证据:

Hacker101 Encrypted Pastebin 通关教科书式详解

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

 

我简单画了一个图解,大家可以将上面的公式结合这个图来脑补一下整个解密过程:

Hacker101 Encrypted Pastebin 通关教科书式详解

你可以自己写工具或者使用padbuster,padbuster用法

padbuster http://XXXX/?post= 16 -encoding 0

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…}

配上如下图解

Hacker101 Encrypted Pastebin 通关教科书式详解

大家可以比较直观的看到,我例子用的数据就是第一个区块的密文,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= 16 -encoding 0 16 -encoding 0 -plaintext ‘{“id”: “1”}’ -ciphertext 8e2e290858dd28f84a0705c7bc8596d7 -intermediate 50ffb4b14baf4ba3529b7d4966092669

 

#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这一关成功的人数会不会突然增加…..

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论