2021 OWASP TOP 2:加密失败

一、加密失败漏洞简介

加密是一个数学问题,应用到了开发场景。事实上,加密函数就像一个黑盒,开发人员能够考虑的只有输入和输出,其中输出还是非常复杂的。加密是否成功,极大地影响着系统的安全性,但是很多开发人员,对加密却没有深入研究。所以开发人员在考虑加密的时候,并不验证起加密质量如何,往往只会在意他们的加密输出是否成功。

在国内的信息安全建设大背景下,系统的数据安全已经愈加重要,其中首先要考虑的就是数据的传输层和存储层的安全。这些环节中主要采用的保护方案就是加密,目前加密已经渗透到了开发的方方面面。以下是我们的一些加密应用场景:

  • 数据是否是通过明文进行传输的?
  • 业务系统中是否存在旧版本或者强度比较低的加密函数?
  • 服务器上的证书是否合法有效,证书信任链是否完整?
  • 加密函数的初始化序列是否被合理使用?
  • 是否使用了不安全的加密操作,比如 ECB?
  • 随机数是否得到了合理的初始化,以及是否使用了硬编码种子?
  • 加密错误信息或者侧信道信息是否导致密码可破解?

与之对应的,加密的攻击场景往往如下:

  • 数据库不安全加密
  • 数据明文传输
  • 加密强度不够
  • 弱hash
  • 签名验证不当

二、实战场景—RSA低加密指数攻击

RSA加密回顾

在研究该攻击方法之前,我们先复习一眼RSA加密过程:

plain_text = 明文,cipher_text = 密文,(n,e) = 公钥,(n,d) = 私钥
加密过程:plain_text ^ e ≡ cipher_text (mod n)
解密过程:cipher_text ^d ≡ plain_text (mod n)

对于低加密指数攻击,我们的一直条件是:

密文:cipher_text;
公钥:(n,e)。

攻击目标是获取到明文信息(plain_text).

案例实战

实战场景在谜团靶场中,进入靶场我们选择一个RSA-低加密指数攻击场景开启即可:

启动环境后,我们可以在home目录下找到两个文件:flag.enc和pubkey.pem

total 16
-rw-rw-r--@ 1 hunter  staff  512  6  2  2019 flag.enc
-rw-rw-r--@ 1 hunter  staff  796  6  2  2019 pubkey.pem

然后我们通过Openssl对pubkey.pem进行解析:

openssl rsa -pubin -text -modulus -in pubkey.pem
Public-Key: (4096 bit)
...
Exponent: 3 (0x3)
Modulus=B0BEE5E3E9...
...

成功得到n和e,其中n=Moudles,e=Exponent.

我们回看一下加密公式:

plain_text ^ e ≡ cipher_text (mod n)
其中e、n、cipher_text均是已知的,进行一下简单的格式变换可以得出
plain_text = (kn + (cipher_text mod n)) ^ 1/3

然而,对于低加密指数攻击,我们知道,当e很小的时候,我们是可以进行暴力破解攻击的:

接下来我们通过如下代码来进行暴力破解攻击:

import os, time,gmpy2

def main():
    start_time = 0
    c_time = 0
    n = 721059527572145959497866070657244746540818298735241721382435892767279354577831824618770455583435147844630635953460258329387406192598509097375098935299515255208445013180388186216473913754107215551156731413550416051385656895153798495423962750773689964815342291306243827028882267935999927349370340823239030087548468521168519725061290069094595524921012137038227208900579645041589141405674545883465785472925889948455146449614776287566375730215127615312001651111977914327170496695481547965108836595145998046638495232893568434202438172004892803105333017726958632541897741726563336871452837359564555756166187509015523771005760534037559648199915268764998183410394036820824721644946933656264441126738697663216138624571035323231711566263476403936148535644088575960271071967700560360448191493328793704136810376879662623765917690163480410089565377528947433177653458111431603202302962218312038109342064899388130688144810901340648989107010954279327738671710906115976561154622625847780945535284376248111949506936128229494332806622251145622565895781480383025403043645862516504771643210000415216199272423542871886181906457361118669629044165861299560814450960273479900717138570739601887771447529543568822851100841225147694940195217298482866496536787241

    k = 0

    c_path = os.getcwd()
    fname = c_path + "/flag.enc"
    print(fname)
    f = open(fname, 'rb')
    c = f.read()
    c_num = int.from_bytes(c, byteorder='big')

    mod_num = c_num % n
    print('n = ' + str(n))
    print('mod = ' + str(mod_num))
    start_time = int(time.time())
    while True:

        c_time = int(time.time())
        time_pass = c_time-start_time
        if (c_time - start_time) == 10:
            print("current k: " + str(k))
            start_time = c_time

        y = k * n + mod_num
        root_num, status = gmpy2.iroot(y,3)

        if status == 1:break
        else:
            k = k + 1
    print('plain_text = ' + str(root_num))

if __name__ == "__main__":
    main()

等个大约5分钟,我们就能看到暴力破解出来的明文信息了,不过是进行了编码的,需要我们转换一下:


plain_text = 440721643740967258786371951429849843897639673893942371730874939742481383302887786063966117819631425015196093856646526738786745933078032806737504580146717737115929461581126895844008044713461807791172016433647699394456368658396746134702627548155069403689581548233891848149612485605022294307233116137509171389596747894529765156771462793389236431942344003532140158865426896855377113878133478689191912682550117563858186

通过代码将 plain_text 值转换为字符:

def main():
    plain_text = 440721643740967258786371951429849843897639673893942371730874939742481383302887786063966117819631425015196093856646526738786745933078032806737504580146717737115929461581126895844008044713461807791172016433647699394456368658396746134702627548155069403689581548233891848149612485605022294307233116137509171389596747894529765156771462793389236431942344003532140158865426896855377113878133478689191912682550117563858186

    plain_text_in_char = []

    while plain_text != 0:
        plain_text, c = divmod(plain_text, 256)
        plain_text_in_char.append(chr(c))

    plain_text_in_char.reverse()

    print(''.join(plain_text_in_char))

if __name__ == "__main__":
    main()

成功得到明文信息:

Didn't you know RSA padding is really important? Now you see a non-padding message is so dangerous. And you should notice this in future.Fl4g: PCTF{Sm4ll_3xpon3nt_i5_W3ak}

三、实战场景—弱编码

字符编码基础

丰富的编码字符集合:

早期标准:ASCII、EBCDIC
西欧标准:ISO-8859-1、ISO-8859-5、ISO-8859-6、ISO-8859-7、ISO-8859-11、ISO-8859-15 等
DOS 字符集:CP437、CP737、CP850 等
Windows 字符集:Windows-1250、Windows-1251、Windows-1252 等
中文字符集:GB2312、GBK 等
Unicode:Unicode、UTF-7、UTF-8、UTF-16、UTF-32 等

常见的编码方式:

  • URL编码:把字符的 ASCII 值表示为两个 16 进制的数字,然后在其前面放置转义字符%,对于非 ASCII 字符则先转换为 UTF-8 字节序,然后再放置转义字符%。
  • Base64编码:Base64 是一种用 64 个字符来表示二进制数据的方法。由于 64 = 2 ^ 6,因此每 6 位可映射到一个可打印字符,又由于每 6 位等于四分之三字节,因此可以简单理解为每四分之三字节映射到一个新的字节。对于不能被3整除的字节数,末尾补“0”再编码。

案例实战

宽字节注入:

在使用PHP连接MySQL的时候,当设置“set
character_set_client = gbk”时会导致一个编码转换的问题,也就是我们熟悉的宽字节注入,当存在宽字节注入的时候,注入参数里带入% DF%27,即可把(%5C)吃掉

正常URL:

http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1

当我们提交:

id=1' and 1=1%23

此时mysql执行的语句是:

select * from user where  id ='1\' and 1=1#'

很明显这是没有注入成功的,而当我们提交

id=1%df' and 1=1%23

此时mysql执行的语句是:

select * from user where id ='1運' and 1=1#'

此时因为mysql不识别“運”,会将其当做空,所以就成功的被注入。

四、实战场景—证书伪造

关于证书的基础知识

证书,全称又叫做数字证书,是一种基于公钥认证体系的电子文件,用于证明公钥持有者的身份。

证书一般包含以下信息:

  • 公钥信息
  • 拥有者身份
  • 数字证书认证机构对该证书的数字签名

证书的颁发:

证书的颁发,往往是一个商业行为,需要证书颁发机构CA进行颁发,CA是公钥基础设施的核心,负责签发证书、认证证书、管理证书。

基本颁发流程:服务商想CA提出申请--》CA验证和确认服务商身份,通过审核并将公钥信息与用户身份绑定--》CA 为绑定了身份的公钥进行签名,得到证书。

但是证书颁发机构如果直接面向所有的服务商,无疑工作量巨大,所以为了解决这一问题,往往会引入中间商的概念,中间商只要持有根证书机构签发的中介证书,就有给服务商授权证书的权利。这里服务商获得的证书叫终端实体证书。

证书的验证:

证书的验证涉及到的是一个信任链的问题,当用户访问一个网站时,浏览器会执行路径验证算法,使用网站提供的证书去对应系统中预安装的根证书,通过验证两者是否匹配来确定证书是否合法。

根证书:证书颁发机构的公钥证书,用于签名客户公钥信息。

风险产生场景:信任链中断、证书信任链的不正确回溯

  • 信任链中任意一个非根节点的证书是自签发
  • 没有完成整个信任链中每个节点的检查;
  • 信任链中的某个节点证书缺失一些基础信息或者额外的重要扩展信息;
  • 信任链中上层节点证书失效或者被攻击者窃取。

案例实战

中间人攻击就是点心的证书不安全造成的。

此处我们以中间人攻击中的代理不安全为例进行分析。

我们先找一个需要SSL握手的网站,即https网站,比如csdn:https://www.csdn.net/

在这里插入图片描述

正常情况下,我们可以看到,网站证书第有效的。

此时我们通过burpsuite 来抓取我们的csdn的流量,浏览器显示链接不是私密的,被检测出存在代理:

在这里插入图片描述

但是我们把burpsuite的证书导入浏览器,让其信任我们的代理服务器,再重新抓包,发现能够成功的抓取了csdn的流量包,这就是典型的中间人攻击,属于证书校验信任链不完全导致的问题.

在这里插入图片描述

五、实战场景—弱随机数

弱随机数的基本概念

什么是随机数?

随机数最基本的概念是统计学意义上的伪随机数,对于给定的一个样本集,每个元素出现的概率是大概相似的,只要从人类的视角看上去一组数是随机的,就符合统计学意义上的伪随机数定义

因为统计学上的伪随机数,在给定随机样本和随机算法的情况下,能够有效地演算出随机样本的剩余部分,因此统计学上的伪随机数需要得到进一步地安全强化,密码学安全的伪随机数应运而生。

伪随机数的生成:

产生随机数的方法被称为随机数生成器(RNG, random number generator)。
实际应用中我们往往使用伪随机就足够了,这些随机数主要通过一个固定的、可重复的计算方法生成,这些计算方法经过特殊的设计,使得产生的结果具有类似真随机数的统计学特征。这种生成的伪随机数一般只是重复的周期比较大的数列,以算法和种子值共同作用生成

密码学上安全的伪随机数:

密码学上安全的伪随机数,往往要求其具有随机数的特征,而不是类似于伪随机数那样的固定随机列表。

如何提高伪随机数的安全性:

一种方法是将账号信息与时间戳进行绑定,作为随机数生成的种子,在时间序列上实现随机。

此外,使用强的伪随机数算法,比如基于数学安全问题的伪随机数。

安全实践

以 CVE-2019-10908 为例,我们来看一下开源项目的修复方案。

首先从包的使用方面,取消了org.apache.commons.lang.RandomStringUtils的使用,并且替换为java.security.SecureRandom,根据名字很容易判断新替换的包是一个密码学安全的伪随机数生成器,该随机数生成器从算法设计角度上是密码学安全的,同时内部所使用的种子值强度也会更高。

java.security.SecureRandom实现代码:

private static final String SYMBOLS = "abcdefghi jklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
private final SecureRandom random = new SecureRandom();
private static final int PASSWORD_LENGTH = 32;
    // ...
    StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
    for(int i=0; i<PASSWORD_LENGTH; i++) {
    
    
        int index = random.nextInt(SYMBOLS.length());
        sb.append(SYMBOLS.charAt(index));
    }
    String password = sb.toString();

简单来说,就是在开发过程中要通过安全的加密库来使用 CPRNG。

六、实战场景—Hash未加盐

关于hash和加盐的简单理解

Hash的应用:

HASH 函数具备很多优秀的特性,比如计算不可逆、难以伪造、数据压缩等,因此它具备很多应用场景。

基于计算不可逆和难以伪造这两个优秀的安全特性,HASH 函数的一个主要应用场景,就是校验数据传递的完整性。将要传递的数据作为 HASH 函数的输入,生成散列值 A;再在接收端将 A 作为 HASH 函数的输入,生成散列值 B。通过对比 A 和 B,就可以快速判断出数据传输的完整性和数据的真实性。

基于此方法,hash往往被用于计算软件、文件的完整性和真实性,另外,在密码存储方面也常常被用于账户密码的安全存储。

关于“盐“的简单介绍

用户账户认证过程,通常涉及到密码的存储,这就是盐的主要应用场景之一。

我们都知道,密码的存储通常是放在数据库中,关于密码的存储形态有很多种,通常可选的方案包括明文、散列值等。

采用明文存储的方案,一旦发生了入侵事件,或者系统存在漏洞使得数据库外泄,就会导致大规模的用户账户外泄,这种安全事件是灾难性的。所以目前大部分系统采取的方案都是存储散列值,在这种情况下,即使是系统也无法得知用户的密码是什么。

但是随着目前彩虹表攻击的日渐流行,以及公开彩虹变的数据愈加庞大,直接使用hash进行数据传输和存储已经不在安全,而加盐则顺势成为了对抗彩虹变攻击的有效方法。

实战案例

某客户的某系统中,采用了简单的MD5算法来传输用户登录密码:

在这里插入图片描述

在存在中间人攻击的情况下,一旦MD5后的密码内容被截取,就很容易通过hash彩虹表找到对应的明文密码:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45590334/article/details/125671435