PHP的学习–PHP加密

前言

PHP是一种通用的开源脚本语言,它的语法混合了C,Java,以及Perl等优秀语言的语法。除此之外,它还提供了大量的函数库可供开发人员使用。但是,如果使用不当,PHP也会给应用程序带来非常大的安全风险。

在这篇文章中,我们将会对PHP应用程序中经常会出现的一些问题进行深入地分析,尤其是当我们使用“==”(比较运算符)来进行字符串比较时,可能会出现的一些安全问题。虽然近期有很多文章都围绕着这一话题进行过一些探讨,但我决定从“黑盒测试”的角度出发,讨论一下如何利用这个问题来对目标进行渗透和攻击。首先,我会对引起这个问题的根本原因进行分析,以便我们能够更加深入地理解其工作机制,这样才可以保证我们能够尽可能地避免这种安全问题的发生。

PHP中的加密方式有如下几种

问题的描述

在2011年,PHP官方漏洞追踪系统发现,当字符串与数字在进行比较的时候,程序会出现某些非常奇怪的现象。从安全的角度出发,这个问题实际上并不能算是一个安全问题。比如说,你可以看到下面这段代码:

图片 1

实际上,当使用类似“==”这样的比较运算符进行操作时,就会出现这样的情况。上面这个例子中出现的问题不能算是一个漏洞,因为它是PHP所提供的一种名为“类型转换”的功能。从本质上来分析,当我们使用特定的比较运算符(例如==
, !=,
<>)来进行操作时,PHP首先会尝试去确定参与比较的数据类型。但是这样的一种类型转换机制将有可能导致计算结果与我们预期的结果有较大出入,而且也会带来非常严重的安全问题。安全研究专家在该问题的完整披露报告中写到:这种类型转化机制将有可能导致权限提升,甚至还会使程序的密码验证过程变得不安全。

Gynvael写过一篇关于这一话题的经典文章,PHP等号运算符“==”所涵盖的数据类型非常广泛,我们给大家提供了一个较为完整的比较参考列表,并给出了一些示例,具体内容如下所示:

图片 2

正如你所看到的,当我们使用“==”来比较这些数字字符串时,参与比较的就是字符串中数字的实际大小,从安全的角度出发,这就是一个非常有趣的问题了。在这种情况下,你可以使用科学计数法来表示一个数字,并将其放在一个字符串中,PHP将会自动把它作为一个数字类型来处理。我们之所以会得到这样的输出类型,是因为PHP使用了一种哈希算法(通常使用十六进制数值表示)来进行处理。比如说,如果一个数字为0,那么在进行松散比较的过程中,PHP会自动对其类型进行转换,但其值永远为0。对于一个给定的散列算法而言,密码就有可能会变成可以被替换的了。比如说,当密码的哈希值被转换成使用科学计数法来表示的数字时,将有可能正好与其他的密码哈希相匹配。这样一来,即使是一个完全不同的密码,也有可能可以通过系统的验证。但有趣的是,当某些采用科学计数法表示的数字在进行比较的时候,结果可能会让你意想不到:

图片 3

1. MD5加密

string md5 ( string $str [, bool $raw_output = false ] ) 

参数

str  —  原始字符串。

raw_output  —  如果可选的 raw_output 被设置为 TRUE,那么 MD5
报文摘要将以16字节长度的原始二进制格式返回。

这是一种不可逆加密,执行如下的代码

$password = '123456';  echo md5($password);

得到结果是e10adc3949ba59abbe56e057f20f883e

从“黑盒测试”的角度出发来考虑这个问题

从静态分析的角度来看,这些安全问题就显得有些普通了。但如果我们从黑盒的角度来看待这些问题,我们能够得到什么样的启发呢?对于应用程序中的任何用户账号而言,如果应用程序使用了当前最为流行的哈希散列算法(例如SHA1和MD5)来对密码进行处理,而你在对密码哈希进行验证的时候使用了PHP的松散比较,那么此时就有可能出现安全问题。我们现在可以考虑进行一次典型的渗透测试,你可以创建一个普通的账号,将密码设置成哈希值类似的其中一个密码,然后使用其他的密码进行登录操作。很明显,系统的安全性完全取决于你所使用的散列算法。所以,我们假设你没有在散列算法中使用“Salt”值,那么你至少得使用两种不同的散列算法来对密码进行处理。

现在,在我们去对这些密码组合进行研究之前,我们还应该考虑到一点——即密码的要求。因为我们在对这些密码和散列算法进行分析之前,首先得确保我们所设置的初始密码复合了密码复杂度的要求,否则我们的分析和研究将会没有任何的意义。因此,我们得确保我们的密码长度至少为八个字符,密码中包含有大小写字母,数字,以及至少一个特殊字符:具体如下所示:

import random
import hashlib
import re
import string
import sys
prof = re.compile("^0+ed*$") # you can also consider: re.compile("^d*e0+$")
prefix = string.lower(sys.argv[1])+'!'+string.upper(sys.argv[1])+"%s"
num=0
while True:
    num+=1
    b = hashlib.sha256(prefix % num).hexdigest()
    if (b[0]=='0' and prof.match(b)):
        print(prefix+str(num),b)

为此,我专门编写了一个Python脚本,虽然我没有竭尽全力去优化这个脚本的性能,但是在PyPy编译器的帮助下,这个精心编写的脚本可以在我的AMD
FX8350所有可用的CPU核心中稳定运行。除此之外,我还使用到了hashlib库中的散列函数,而且为了避免遇到Python
GIL的进程同步问题,我还生成了独立的进程来对密码数据进行处理。不仅如此,我还使用了非常复杂的技术来为每一个密码生成不同的前缀,正如上面这段代码所示。

2. Crype加密

string crypt ( string $str [, string $salt ] )

crypt() 返回一个基于标准 UNIX DES
算法或系统上其他可用的替代算法的散列字符串。

参数

str  —  待散列的字符串。

salt  —
 可选的盐值字符串。如果没有提供,算法行为将由不同的算法实现决定,并可能导致不可预料的结束。

这是也一种不可逆加密,执行如下的代码

$password = '123456';  $salt = "test";// 只取前两个  echo crypt($password, $salt);

得到的结果是teMGKvBPcptKo

使用自动盐值的例子如下:

$password = crypt('mypassword'); // 自动生成盐值    /* 你应当使用 crypt() 得到的完整结果作为盐值进行密码校验,以此来避免使用不同散列算法导致的问题。(如上所述,基于标准 DES 算法的密码散列使用 2 字符盐值,但是基于 MD5 算法的散列使用 12 个字符盐值。)*/  if (crypt('mypassword', $password) == $password) {     echo "Password verified!";  }

执行结果是输出 Password verified!

以不同散列类型使用 crypt()的例子如下:

if (CRYPT_STD_DES == 1) {      echo 'Standard DES: ' . crypt('rasmuslerdorf', 'rl') . "n";  }    if (CRYPT_EXT_DES == 1) {      echo 'Extended DES: ' . crypt('rasmuslerdorf', '_J9..rasm') . "n";  }    if (CRYPT_MD5 == 1) {      echo 'MD5:          ' . crypt('rasmuslerdorf', '$1$rasmusle$') . "n";  }    if (CRYPT_BLOWFISH == 1) {      echo 'Blowfish:     ' . crypt('rasmuslerdorf', '$2a$07$usesomesillystringforsalt$') . "n";  }    if (CRYPT_SHA256 == 1) {      echo 'SHA-256:      ' . crypt('rasmuslerdorf', '$5$rounds=5000$usesomesillystringforsalt$') . "n";  }    if (CRYPT_SHA512 == 1) {      echo 'SHA-512:      ' . crypt('rasmuslerdorf', '$6$rounds=5000$usesomesillystringforsalt$') . "n";  }

其结果如下

Standard DES: rl.3StKT.4T8M  Extended DES: _J9..rasmBYk8r9AiWNc  MD5:          $1$rasmusle$rISCgZzpwk3UhDidwXvin0  Blowfish:     $2a$07$usesomesillystringfore2uDLvp1Ii2e./U9C8sBjqp8I90dH6hi  SHA-256:      $5$rounds=5000$usesomesillystri$KqJWpanXZHKq2BOB43TSaYhEWsQ1Lr5QNyPCDH/Tp.6  SHA-512:      $6$rounds=5000$usesomesillystri$D4IrlXatmP7rx3P3InaxBeoomnAihCKRVQP22JZ6EY47Wc6BkroIuUUBOov1i.S5KPgErtP/EN5mcO.ChWQW21

在 crypt()
函数支持多重散列的系统上,下面的常量根据相应的类型是否可用被设置为 0 或
1:

  • CRYPT_STD_DES – 基于标准 DES 算法的散列使用 “./0-9A-Za-z”
    字符中的两个字符作为盐值。在盐值中使用非法的字符将导致 crypt()
    失败。
  • CRYPT_EXT_DES – 扩展的基于 DES 算法的散列。其盐值为 9
    个字符的字符串,由 1 个下划线后面跟着 4 字节循环次数和 4
    字节盐值组成。它们被编码成可打印字符,每个字符 6
    位,有效位最少的优先。0 到 63 被编码为
    “./0-9A-Za-z”。在盐值中使用非法的字符将导致 crypt() 失败。
  • CRYPT_MD5 – MD5 散列使用一个以 $1$ 开始的 12 字符的字符串盐值。
  • CRYPT_BLOWFISH – Blowfish 算法使用如下盐值:“$2a$”,一个两位 cost
    参数,“$” 以及 64 位由 “./0-9A-Za-z”
    中的字符组合而成的字符串。在盐值中使用此范围之外的字符将导致 crypt()
    返回一个空字符串。两位 cost 参数是循环次数以 2
    为底的对数,它的范围是 04-31,超出这个范围将导致 crypt() 失败。
  • CRYPT_SHA256 – SHA-256 算法使用一个以 $5$ 开头的 16
    字符字符串盐值进行散列。如果盐值字符串以 “rounds=<N>$” 开头,N
    的数字值将被用来指定散列循环的执行次数,这点很像 Blowfish 算法的
    cost 参数。默认的循环次数是 5000,最小是 1000,最大是
    999,999,999。超出这个范围的 N 将会被转换为最接近的值。
  • CRYPT_SHA512 – SHA-512 算法使用一个以 $6$ 开头的 16
    字符字符串盐值进行散列。如果盐值字符串以 “rounds=<N>$” 开头,N
    的数字值将被用来指定散列循环的执行次数,这点很像 Blowfish 算法的
    cost 参数。默认的循环次数是 5000,最小是 1000,最大是
    999,999,999。超出这个范围的 N 将会被转换为最接近的值。

分析结果

在经过了一个多小时的分析之后,我得到了四个密码的SHA1值。令我感到惊讶的是,得到四个密码的MD5值所需的时间竟然更短。

密码的计算结果十分相似,具体如下所示:

图片 4

你可以随意选取两个密码来进行对比,对比的演示结果如下:

图片 5

如果你无法得到如上图所示的计算结果,那么你应该感到幸运。你可以尝试将用户名和密码捆绑在一起,然后使用带“salt”值的散列算法来进行计算。你只需要修改一小部分代码即可实现,点击“这里”获取修改后的脚本。

3. Sha1加密

string sha1 ( string $str [, bool $raw_output = false ] )

参数

str  —  输入字符串。

raw_output  —  如果可选的 raw_output 参数被设置为 TRUE,那么 sha1
摘要将以 20 字符长度的原始格式返回,否则返回值是一个 40
字符长度的十六进制数字。

这是也一种不可逆加密,执行如下代码:

$password = '123456';  echo sha1($password);

得到的结果是7c4a8d09ca3762af61e59520943dc26494f8941b

以上几种虽然是不可逆加密,但是也可以根据查字典的方式去解密。如下的地址中就提供了可以将上面的加密结果解密出来的功能。

那大家是不是加了就算加了密,也没用啊,其实不然,只要你的加密足够复杂,被破解出的可能性就越小,比如用以上三种加密方式混合加密,之后我会推荐给大家一个php的加密库。

解决方案

PHP给我们提供了一个解决方案,如果你想要对比哈希值,你应该使用password_verify()或hash_equals()这两个函数。它们会对数据进行严格比较,并排除一些其他的干扰因素。但是请你注意,hash_equals()函数也可以用于字符串的比较。

4. URL加密

string urlencode ( string $str ) 

此函数便于将字符串编码并将其用于 URL
的请求部分,同时它还便于将变量传递给下一页。

返回字符串,此字符串中除了 -_.
之外的所有非字母数字字符都将被替换成百分号(%)后跟两位十六进制数,空格则编码为加号(+)。此编码与
WWW 表单 POST 数据的编码方式是一样的,同时与
application/x-www-form-urlencoded
的媒体类型编码方式一样。由于历史原因,此编码在将空格编码为加号(+)方面与
RFC1738 编码不同。

string urldecode ( string $str )

解码给出的已编码字符串中的任何 %##。 加号(’+’)被解码成一个空格字符。

这是一种可逆加密,urlencode方法用于加密,urldecode方法用于解密,执行如下代码:

$url = 'http://www.cnblogs.com/CraryPrimitiveMan/';  $encodeUrl = urlencode($url);  echo $encodeUrl . "n";// 如果是在网页上展示的,就将n修改为<br/>  echo urldecode($encodeUrl);

得到的结果如下

http%3A%2F%2Fwww.cnblogs.com%2FCraryPrimitiveMan%2F  http://www.cnblogs.com/CraryPrimitiveMan/

基于RFC 3986的加密URL的方法如下:

function myUrlEncode($string) {      $entities = array('%21', '%2A', '%27', '%28', '%29', '%3B', '%3A', '%40', '%26', '%3D', '%2B', '%24', '%2C', '%2F', '%3F', '%25', '%23', '%5B', '%5D');      $replacements = array('!', '*', "'", "(", ")", ";", ":", "@", "&", "=", "+", "$", ",", "/", "?", "%", "#", "[", "]");      return str_replace($entities, $replacements, urlencode($string));  }

分析结论

虽然我们的分析步骤执行起来有些过于复杂,但是从黑盒测试的角度出发,我们所描述的方法也许可以给大家提供一些有价值的信息。如果某个应用程序中的密码采用了这样的一种验证机制,那么它所带来的安全问题将会超出PHP数据类型转换本身所存在的问题。

5. Base64信息编码加密

string base64_encode ( string $data ) 

使用 base64 对 data 进行编码。

设计此种编码是为了使二进制数据可以通过非纯 8-bit
的传输层传输,例如电子邮件的主体。

Base64-encoded 数据要比原始数据多占用 33% 左右的空间。

string base64_decode ( string $data [, bool $strict = false ] ) 

对 base64 编码的 data 进行解码。

参数

data  —  编码过的数据。

strict  —  如果输入的数据超出了 base64 字母表,则返回 FALSE。

执行如下代码:

$name = 'CraryPrimitiveMan';  $encodeName = base64_encode($name);  echo $encodeName . "n";  echo base64_decode($encodeName);

其结果如下

Q3JhcnlQcmltaXRpdmVNYW4=  CraryPrimitiveMan

问题远不止于此

这个问题给我们带来的影响远远不止于此。攻击者可以将这些密码添加到字典文件中,然后对应用程序中的所有用户进行暴力破解攻击。而且,如果应用程序的密码恢复机制中存在不安全的因素,攻击者还有可能对目标账号进行不限次数的攻击,直到攻击成功为止。

推荐phpass

经 phpass 0.3 测试,在存入数据库之前进行哈希保护用户密码的标准方式。
许多常用的哈希算法如 md5,甚至是 sha1 对于密码存储都是不安全的,
因为骇客能够使用那些算法轻而易举地破解密码。

对密码进行哈希最安全的方法是使用 bcrypt 算法。开源的 phpass
库以一个易于使用的类来提供该功能。

<?php  // Include phpass 库  require_once('phpass-03/PasswordHash.php')    // 初始化散列器为不可移植(这样更安全)  $hasher = new PasswordHash(8, false);    // 计算密码的哈希值。$hashedPassword 是一个长度为 60 个字符的字符串.  $hashedPassword = $hasher->HashPassword('my super cool password');    // 你现在可以安全地将 $hashedPassword 保存到数据库中!    // 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码  $hasher->CheckPassword('the wrong password', $hashedPassword);  // false    $hasher->CheckPassword('my super cool password', $hashedPassword);  // true  ?>

陷阱

  • 许多资源可能推荐你在哈希之前对你的密码“加盐”。想法很好,但 phpass 在
    HashPassword()
    函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。

进一步阅读

  • phpass
  • 为什么使用 md5 或 sha 哈希密码是不安全的(中文)
  • 怎样安全地存储密码

源代码

点击“这里”获取Python源码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注