浅析图形验证码安全

CoolCat 58 0

0x01 前言

验证码的定义:

验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类。

验证码的作用:

简而言之、防止机器操作。

验证码分为图形验证码、短信验证码、语音验证码等等。本文仅分析图形验证码这一种。

0x02 基本流程

笔者将生成验证码的流程总结如下:

可能存在的问题:

  • 验证码使用后未销毁。
  • 生成验证码的字符集可控。
  • 验证码存放位置暴露。
  • 验证码对比失败后仍进行其他对比。
  • 其他对比后验证码未销毁。
  • 验证码尺寸可控
  • 验证码过于简单,可轻易机器识别。

0x03 代码还原&实例分享

两段简单的PHP代码实现生成数字验证码并验证,以下代码复现均基于这两文件实现:

captcha.php

<?php
    session_start();
    $image=imagecreatetruecolor(100,30);
    $bgcolor=imagecolorallocate($image,255,255,255); 
    imagefill($image, 0, 0, $bgcolor);
    $captch_code='';
    //画出4个随机的数字
    for($i=0;$i<4;$i++){
        $fontsize=10;
        $data='0123456789';//字符集
        $fontcontent=substr($data,rand(0,strlen($data)),1); 
        $captch_code.=$fontcontent;     
        $x=($i*100/4)+rand(5,10);
        $y=rand(5,10);
        imagestring($image,$fontsize,$x,$y,$fontcontent,$fontcolor);
    }
    $_SESSION['captcha']=$captch_code;
    header('content-type:image/png');
    imagepng($image);

    imagedestroy($image);

index.php

<?php 
header("Content-type: text/html; charset=utf-8");
if(isset($_REQUEST['captcha'])){
  session_start();
  if(strtolower($_REQUEST['captcha'])==$_SESSION['captcha']){
    echo '验证码正确';
    $_SESSION['captcha']=null;
  }else{
    echo '验证码错误';
    $_SESSION['captcha']=null;
  }
  exit();
}
?>

<html>
<head>
    <meta charset="UTF-8">
    <title>浅析图形验证码安全</title>
</head>
<body>
    <form method="post" action='./index.php'>
        <p>验证码图片:
          <img id="code_img" border="1" src="./captcha.php?r=<?php echo rand();?>" >
          <a href="javascript:void(0)" onclick="document.getElementById('code_img').src='./captcha.php?r='+Math.random()">刷新一下</a>
        </p>
        <p>请输入验证码:<input type="text" name="captcha" value=""></p>
        <p><input type="submit" value="提交" style="padding:6px 20px;"></p>
    </form>
</body>
</html>

Round 1:验证码使用后未销毁

这是最常见的一种验证码缺陷,通常我们也叫验证码复用,代码如下:

开发常忘记注销 session 中存入的验证码,攻击者可截断请求包,重复利用同一个验证码即可。

案例:

验证码使用后未注销,可截断刷新验证码的请求,直接爆破密码。

Round 2:生成验证码的字符集可控

生成验证码的字符集可控这种情况并不常见,目前笔者接触过的案例出现在ThinkCMF 1.X-2.X,少见,但是脚本化容易,对比上一中优势在于验证码可控,不用人工输入第一个有效 Payload ,代码层面如下:

案例:

详细分析可移步验证码外部控制实例,也是笔者的稿子。

Round 3:验证码存放位置暴露

此类问题出现比例低于第一种高于第二种,出现问题的缓解主要在于开发没把验证码的明文写进session而是选择了写到其他地方,比如写到Cookie,藏到编码后的某些值里面,藏到图片的字节里面等等。代码层面如下:

(藏到Cookie里面,其他情况参考逻辑让我崩溃之验证码姿势分享

案例:

Round 4:验证码对比失败后仍进行其他对比

这类问题出现的频率和上一中相差无几,问题出现在开发的登录逻辑上,验证码失败后本该退出页面,开发只是提醒了一下,并未阻断后续代码的运行,代码实现如下:

案例:

图片截取自某上帝的代码,开发在对比验证嘛失败后checkFlag的值设置错误,导致验证码只要不为空就可以继续对比密码,提示不影响。有些跑题,可参考某KCMS5.0 代码审计中任意用户密码重置部分,逻辑上是一个道理,也是笔者文稿。

Round 5:其他对比后验证码未销毁

这类不常见,但是利用起来很有意思,常出现在有其他校验的地方。代码实现如下:

(仿写某很友好的甲方代码)

验证码并未注销,看起来可以复用,实则不行,因为后面进行了校验,每次hash都不一样,变相的导致了验证码不可复用,不可爆破。

案例:

那么如何利用?如仿写的代码所示,仅仅校验了密码字段加验证码,用户名并未校验,那么这里就可以输入一个弱密码(如123456,111111,123123等等),然后反向去遍历用户,获取正确的口令。

效果:

如图,笔者当时使用123456这个弱密码去遍历用户,成功获取到几十个用户口令。

Round 6:验证码尺寸可控

该问题被人们广为关注源于PHPcms早期版本的后台登录验证码尺寸可控,可用于DDoS。而笔者亲自挖掘到这样的问题源于ThinkCMF 1.X-2.X,详情可参考验证码外部控制实例中的尺寸可控部分。代码实现如下:

案例:

(图片截取自笔者的漏洞报告)

Round 7:验证码过于简单,可轻易机器识别。

写到这里细心的读者会发现,到目前为止给出的代码运行后的真面目都还见过,真面目是这样的:

实战场景中甲方验证码大部分都是在这个的基础上加了一些噪点而已。

识别实例:

0x04 总结

修复套话:

  • 验证码设置为6位并设置超时(一分钟)失效。
  • 建议修改应用程序源代码,在登录模块中增加对验证码正确性的验证,并且要保证在验证用户名、密码或其他信息之前首先验证验证码的正确性。

真实感受:

文中提到的这些验证码的问题在甲方单位中(尤其是事业单位)业务系统比比皆是,更有甚者验证码直接没参与验证,只是放在前端应付一下而已。

广告

打赏
发表评论 取消回复
表情 图片 链接 代码

分享
微信
微博
QQ