深蓝居

关注MS的VS2008和SQL Server 2008

常用链接

统计

积分与排名

朋友

学习生活

最新评论

再论验证码安全:请及时销毁你的验证码

我在上一篇文章中讲到了如何使用C#模拟用户登录具有验证码网站。今天我就换位思考一下,站在网站开发人员的角度讲一讲验证码的的一个安全问题:及时销毁网站中的验证码。

为了方便大家理解,这里我就以一个投票的应用网站为例进行说明。投票网站首先要防止的就是用户不断点击投票按钮来重复投票;当然,避免重复投票的解决办法有很多,比如记录IP、写入Session、Cookie甚至还有要求用户输入身份证号码等。但是你记录IP,那我就写一个程序来模拟发包,每投1票后自动换代理,然后继续投票,如果是写入到Session中那么我写个投票程序,每投1票就重新开启一个新的会话就是。如果是记入Cookie,那我该表Cookie的值再模拟投票发包,要输入身份证号进行验证?写个身份证号码生成程序也十分容易……

在投票机器人面前,记录IP、记录身份证号码、写入Session和Cookie等防作弊技术形同虚设。所以在投票网站中验证码功能必不可少。那么我们将验证码功能加入到网站投票中:

1.生成验证码图片的页面CreateImg.aspx,其后台代码为:

protected void Page_Load(object sender, EventArgs e)
   
{
string checkCode = CreateCode(4);//生成随机是4位验证码
       Session["CheckCode"= checkCode;//将验证码保存到Session中
       CreateImage(checkCode);//将验证码以图片的方式输出
   }
  

2.在用户单击投票按钮后触发的事件:

protected void btnVote_Click(object sender, ImageClickEventArgs e)
{
    
if (!ValidateUserInfo())//验证IP在数据库中的情况,一个IP一天只能投5张票,从而防止重复投票
    {
        UIHelper.Alert(Page, 
"一个IP一天只能投5张票,请勿重复投票");
        
return;
    }

    
if (Session["CheckCode"== null)
    
{
        UIHelper.Alert(Page, 
"验证码已过期,请重新输入");
        
return;
    }

    
if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())//验证码忽略大小写
    {
        UIHelper.Alert(Page, 
"验证码错误");
        
return;
    }

    
else//验证码正确
    {
        UpdateVote();
//修改数据库,将投票数加1
    }

}
 

OK,大功告成!这个程序逻辑上有问题吗?没有吧,验证码是生成的图片,图片是有干扰因素的,不会被程序识别,而且验证码的内容是保存到服务器的,逻辑处理也是错。似乎一切都那么完美,但是事实并不是这样,对于这样的投票网站,我的投票机器人仍然肆无忌惮的不断切换IP,不断刷票。(要做投票机器人的同志们注意啦,不要看到投票的地方是有验证码的就一筹莫展了哦,也许他的网站就存在以下描述的漏洞哦!)

漏洞就出在投票时对验证码进行验证后没有对服务器上Session中的验证码内容进行销毁。在平时使用IE浏览时,每投票一次后刷新页面,验证码生成页面被重新请求,所以Session值在请求验证码生成页时被替换,所以不会有什么问题。但是现在面对的是投票机器人,我的机器人在第一次请求时获得验证码的图片并展示给用户,用户肉眼识别验证码,然后输入程序的文本框中,由于服务器上验证码的内容并没有被销毁,而且投票程序也不会再请求验证码生成图片的URL,所以接下来每次使用相同的SessionID和用户输入的验证码值,服务器验证投票时 
if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())
都会返回false,验证码都是通过的,所以投票自然成功。终究是百密一疏啊!费尽心思防止投票作弊,最终却因为这一个地方的疏忽而前功尽弃,投票作弊成功,投票结果还是被投票机器人所左右。

也许有人想到了,那可以在Session中放置一个标记,如果投票成功了就将标记置“1”,下次请求时判断Session中标记为“1”就拒绝投票就是了。但是投票只是我这里举的一个例子,像论坛这种用验证码防止用户恶意灌水的总不可能限制用户只发一帖吧。论坛发帖时的验证码如果没有被及时销毁,那么我的灌水机器人就仍然可以到处肆意发帖了,哈哈哈哈。

要避免这个漏洞被利用还是很简单,只需要将上面的代码中投票完成后立即将验证码从服务器上销毁即可:

 

protected void btnVote_Click(object sender, ImageClickEventArgs e)
{
    
if (!ValidateUserInfo())//验证IP在数据库中的情况,一个IP一天只能投5张票,从而防止重复投票
    {
        UIHelper.Alert(Page, 
"一个IP一天只能投5张票,请勿重复投票");
        
return;
    }

    
if (Session["CheckCode"== null)
    
{
        UIHelper.Alert(Page, 
"验证码已过期,请重新输入");
        
return;
    }

    
if (Session["CheckCode"].ToString().ToLower() != txbCode.Text.ToLower())//验证码忽略大小写
    {
        UIHelper.Alert(Page, 
"验证码错误");
        
return;
    }

    
else//验证码正确
    {
        UpdateVote();
//修改数据库,将投票数加1
    }


Session[
"CheckCode"= null;//验证码使用后马上从服务器销毁
}

这样如果仍然使用相同的SessionID和验证码值,那么将会在 if (Session["CheckCode"] == null)这里判断出验证码已经过期,想成功投票?重新请求验证码页面获得验证码图片,然后重新输入验证码吧!
这个问题虽然看起来不以为然,但是正所谓“千里之堤毁于蚁穴”,只要验证码没有从服务器上销毁,那么页面上的验证码还是形同虚设,和验证码的图片地址为<img src="CreateCheckCode.aspx?code=af5d" alt="验证码">
这样把验证码直接暴露在HTML中或者直接使用文本而不是图片来表示验证码有什么区别呢?

另外有人提到,这里是Session保存验证码才会有这个问题,那完全基于Cookie加密的呢?在前面的文章中我也提到过Cookie加密的方式保存验证码的内容,但是今天我又仔细想了一下,得出结论:验证码内容不能保存到客户端,也就是说根本就不应该使用Cookie加密的方式,Cookie加密保存验证码明文是没有什么意义的,必须要在服务器端保存与验证码相关的信息(比如验证码明文或者验证码加密解密密钥)。为什么不能使用Cookie加密保存验证码?我举个简单的例子吧:

比如现在页面上显示的验证码是1234,同时抓包发现提交的时候Cookie中有值:“EncryptCode=asdf”这是验证码的明文经过加密后的密文,我不知道加密算法是什么,但是我每次程序提交时就将1234作为验证码的值同时将“EncryptCode=asdf”作为Cookie的一部分发送到服务器,那么服务器将1234加密后与发送过来的Cookie值“asdf”一比较,二者相同,验证通过!!!

所以我认为验证码的明文是不可能完全基于客户端的,必须要在服务器上保存与验证码相关的信息(验证码明文或密钥)。既然要在服务器上保存相关信息,那么就可能出现这个漏洞。当然这是我想遍了所有验证码明文保存的方法后得出的结论,我还不敢拍胸脯说这是100%正确的,如果大家认为这个结论不正确,那希望能够提出具体的情况。

希望大家若做过验证码的都再回头看看自己的验证码内容在服务器上及时销毁没有。这个错误很容易犯,我在某大公司的网站上都发现了这个漏洞,可见犯此错的网站绝对不在少数。

最后希望大家的网站更加安全,更加健壮。

【出自博客园深蓝居,转载请注明作者出处】

posted on 2008-06-04 03:38 深蓝 阅读(1833) 评论(29)  编辑 收藏 所属分类: Web开发.Net开发杂项

评论

#1楼  2008-06-04 10:31 Microshaoft      

又没有服务器端时间戳

cookie = Encrypt(VerifyCode + ServerTimeStamp)   回复  引用  查看    

#2楼  2008-06-04 10:40 amingo      

不错!   回复  引用  查看    

#3楼  2008-06-04 10:46 簡簡單單..      

谢谢提醒..   回复  引用  查看    

#4楼 [楼主] 2008-06-04 11:14 深蓝      

@Microshaoft
用服务器时间戳?这个我不知道怎么做,时间戳是一直变化的吧?如果是,那么还是需要将时间戳保存到Session中才能在用户提交的时候进行验证。如果不是,那么每次发相同的Cookie值和验证码值就通过验证了。
  回复  引用  查看    

#5楼  2008-06-04 12:03 求知无傲      

学习   回复  引用  查看    

#6楼  2008-06-04 12:42 PerfectDesign      

@深蓝
microshaoft讲的其实就是及时过期cookie,跟你这个讲的一个样
及时销毁,就是及时过期你的cookie就可以   回复  引用  查看    

#7楼  2008-06-04 12:55 菜菜灰      

受教了,非常感谢~   回复  引用  查看    

#8楼 [楼主] 2008-06-04 13:33 深蓝      

@PerfectDesign
哦。这样啊。能不能给出一下基于时间戳的的验证码验证的示例代码,我不知道时间戳怎么做。谢谢。   回复  引用  查看    

#9楼  2008-06-04 13:43 boot [未注册用户]

请教楼主一个问题,不重新拨号,不用代理,如何让机器人切换IP?我看到有人确实可以做到。   回复  引用    

#10楼  2008-06-04 14:00 柳永法 [未注册用户]

楼主无意中提供了好几种方案   回复  引用    

#11楼  2008-06-04 14:04 airwolf2026      

不错.关注下   回复  引用  查看    

#12楼 [楼主] 2008-06-04 14:14 深蓝      

@boot
要切换IP很容易,操作系统提供了netsh命令用于重新设置本机的IP,可以用程序来调用该命令。如果要完全自己写程序来实现改IP我还不知道怎么做,估计得调用Windows的API吧。   回复  引用  查看    

#13楼  2008-06-04 14:26 PerfectDesign      

@深蓝
encrype(verifycode+datime.now.addminites(1))
下次你在decrypt的时候就比对这个时间,如果超过1分钟,那么验证码就过期了

这样机器人就不能拿一个验证码对(肉眼看到的验证码和cookie里面存储的加密串)用一分钟以上了。   回复  引用  查看    

#14楼  2008-06-04 14:28 小猪凯      

受教

销毁session的问题以后俺也要注意了.   回复  引用  查看    

#15楼  2008-06-04 15:08 Microshaoft      

@深蓝
Session 是否过期不重要
Cookie 是否过期不重要

关键是:
Encrypt(VerifyCode + ServerTimeStamp)
解密后判断里面的时间戳是否过期!
超时后防止重放

使用Cookie可以支持负载均衡   回复  引用  查看    

#16楼 [楼主] 2008-06-04 15:33 深蓝      

@PerfectDesign
@Microshaoft
的确是一个不错的办法,直接限制了验证码在生成后n分钟内有效。不过n值不好权衡,太长了(哪怕是只有1分钟)还是容易被机器人利用(一个多线程的投票机器人一分钟内投几百票还是没有问题的),太短了用户体验又不是很好。   回复  引用  查看    

#17楼  2008-06-04 18:14 airwolf2026      

楼主说的用netsh切换IP只是本地的(目前国内的网络环境哈)   回复  引用  查看    

#18楼  2008-06-04 20:47 菜菜灰      

还是没有太明白时间戳的概念,c#有这个方法吗?
Encrypt   回复  引用  查看    

#19楼  2008-06-04 21:01 菜菜灰      

另外问下
Session["CheckCode"] = null;

Session.Remove("CheckCode");

有什么区别,我觉得销毁应该使用下面的方法~   回复  引用  查看    

#20楼  2008-06-04 22:48 niat_alex      

学习了 ~~~   回复  引用  查看    

#21楼  2008-06-05 07:04 小灰 [未注册用户]

我也发表过类似文章:http://www.svnhost.cn/Article/Detail-34.shtml   回复  引用    

#22楼  2008-06-05 19:17 这个文章不应该公布111 [未注册用户]

11111111这篇文章不该公布出来。 你没发现很多人是带着搞破坏的心情来的嘛?

文章是好文章,但是公布出来会造成更大的混乱。

就好像你打开了一个潘多拉盒子。

希望你认真想想这个问题。

最好不要公开。

我填的信箱是真实的, 有什么事可以和我讨论。 来信要注明你的身份

PS 这个验证码安全的问题我早已经发现了。现在正筹划一个有关验证码安全解决方案的项目。有兴趣可以联系我。   回复  引用    

#23楼 [楼主] 2008-06-06 01:39 深蓝      

@这个文章不应该公布111
我不这样认为,我的这篇博客的目的是提高网站开发人员的安全意识,减少这种错误的发生。而且我发布在博客园上,而不是什么所谓的黑客论坛上,博客园中大部分人都是做网站开发的,我觉得做网站开发就应该要知道这些。
科学也是一把双刃剑,科学的进步也给社会造成更大的威胁,难道就因为这样大家就不去学习科学技术了吗?科学就不发展了吗?不,相反我们要更广泛的普及科学知识,让更多的人了解科学。   回复  引用  查看    

#24楼  2008-06-06 08:57 你错了 [未注册用户]

我们是为了改进,可不应该在没有一个切实有效的解决方法前来公布,

很多大公司都是在解决问题以后才发行补丁并公布的。

考虑问题要从大局出发。

我们是要为了防止出现问题,而不是为了制造更大的麻烦。

比如就你的解决方案来说,你有没有考虑到如果在应用中不可以销毁SESSION

该怎么解决? 还有这样的解决方案依然不能改变验证码识别技术破解的现状。   回复  引用    

#25楼  2008-06-06 15:03 S.Sams      

切换IP得用代理, 切换本机的IP, 对外网IP是没有任何改变的.   回复  引用  查看    

#26楼  2008-06-07 19:48 1111111111 [未注册用户]

博客园已经启用这种技术了   回复  引用    

#27楼  2008-06-15 13:33 user001 [未注册用户]

很高深吗?

不见得,更多的你们还不知道呢.   回复  引用    

#28楼  2008-06-15 14:54 rex xiang      

这个...其实是个很老的程序漏洞了吧.
至少我2年前就看到过相关的文章,的确需要提醒一下,很多时候都容易被人忽略掉.   回复  引用  查看    

#29楼  2008-07-29 11:13 benlei [未注册用户]

是啊,在一些关键的业务中的确需要考虑这个问题   回复  引用    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-06-04 10:13 编辑过


相关链接:

历史上的今天:
2007-06-04 Javascript设置对象的ReadOnly属性
 

我要啦免费统计