在 Web 枚举账户时会遇到验证码,通过二次验证导致工作无法继续,验证码是爆破中最大的拦路虎。

目录

图片验证码

验证码生成方案:

  1. 产生随机数
  2. 将随机数写入图片
  3. 将随机数写入 SESSION
  4. 返回图片给前端
<?php

session_start();

$width = !(empty($_GET['width'])) ? $_GET['width'] : '50';
$height = !(empty($_GET['height'])) ? $_GET['height'] : '34';

$image = imagecreate($width, $height);
$bcolor = imagecolorallocate($image, 0, 0, 0); // 验证码图片底色
$fcolor = imagecolorallocate($image, 255, 255, 255); // 验证码颜色

$str = '0123456789';

// 验证码
$rand_str = '';

// 随机取出4位数
for ($i = 0; $i < 4; ++$i) {
    $k = mt_rand(1, strlen($str)); // 4
    $rand_str .= $str[$k - 1];
}

// 验证码写入到SESSION
$_SESSION['verifycode'] = $rand_str;

imagefill($image, 0, 0, $bcolor);
imagestring($image, 7, 7, 10, $rand_str, $fcolor);

header('content-type:image/png');
imagepng($image);

验证码验证方案:

  1. 检查传输过来的验证码是否为空
  2. 检查验证码是否与 SESSION 中存入的验证码一致
  3. 验证码比对错误或者其他业务逻辑错误(比如登录、查询等),则滞空验证码,比如 null 或者空字符串。

常见逻辑错误

  1. 验证错误不重新生成验证码,或者清空验证码。修复思路:在密码错误后将验证码滞空,下次登陆时只要你传来的验证码是空字符串则返回错误,不是空的肯定和空字符串比对产生错误。
  2. 活取到的验证码响应输出结果
  3. 验证码有效期过长
  4. 验证码 4 位数长度过短可以爆破
  5. 验证码可以重复使用
  6. 验证码只是判断不为空,随意输入字符不校验具体内容
  7. 验证码混淆程度不够如四则运算、数字、数字加字符验证码可以被 OCR 识别,可以搞个本地机器学习去解决此问题,成本很低,github 项目:git4woo/reCAPTCHA。像点击、滑动验证码听说部分黑灰产有解决方案,暂时无法搞定。
  8. 万能验证码,开发为了方便测试设定了某些验证码为固定值,让它永远是正确的不失效。

    123456
    123123
    000000
    111111
    222222
    333333
    444444
    555555
    666666
    777777
    888888
    999999
    321321
    321123
    123321
  9. 有一些是判断账户错了几次密码,在数据库上加错误次数来显示验证码,限制验证错误次数,可以在用户名处加空格绕过,因为加了空格系统认为这是个全新账户,错误验证次数是零。
  10. 有错误多了就返回 Cookie,这个 Cookie 就是用于标识当前用户需要验证码,把 Cookie 删除即绕过。
  11. 使用 IP 做限制,每次错误就写错误次数到库里。

    PHP 获取 IP 的三种方式(还有其他的比如 CDN IP 头):

    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $id = $_SERVER['HTTP_X_FORWARDED_FOR'];
    } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
    $id = $_SERVER['HTTP_CLIENT_IP'];
    } else {
    $id = $_SERVER['REMOTE_ADDR'];
    }

    由服务器自主发送的请求头可以伪造:Client-IP,X-Forwarded-For,Cdn-Src-Ip。

    无法伪造:REMOTE_ADDR,获取的是 TCP 连接 HOST。

    主要看业务逻辑怎么做,如果没必要取代理或者其他 CDN 头,用 REMOTE_ADDR 即可。

    Chrome 插件:ModHeader 更改请求头。

  12. 401 验证绕过

    使用 Excel 分 3 列,格式化字典,替换 tab 键:

    Excel 格式化字典.png

    爆破时要注意取消 URL 编码,防止对等号 URL 编码:

    取消特殊字符编码.png

    介绍了 grep 排除错误,这样来确认爆破成功:

    grep 筛选字符.png

图片验证码识别

简单验证码识别,不是调 OCR 付费接口就是,本地机器学习。

短信验证码

短信验证码大部分绕过逻辑和图片验证码差不多,只有写专属逻辑缺陷,可能造成危害。能获取到验证码,获取不到验证码。

短信轰炸

漏洞定义基本规则:同时能发 20 条短信,则算短信轰炸。

利用思路:消耗企业资源,或者骚扰用户,如果能指定短信内容则危害更大。

定向轰炸:对某个手机号定向发送连续多条短信,骚扰用户。

横向轰炸:因为每个手机号能够发送的短信条数优先,所以不断切换手机号,消耗短信资源。

短信发送 Code。

// 发送短信
case 'send_msg':

    // 接收前端传递过来的手机号
    $mobile = $_POST['phone'];

    $GetTime = date('Y-m-d').'%';

    // 查询手机号发送短信日志
    // $db->bind("mobile",$mobile);
    $Query_user_Msg_num = $db->query("SELECT count(*) as num FROM cycc_send_msg_history WHERE time LIKE '$GetTime' AND mobile = $mobile");

    if (end($Query_user_Msg_num)['num'] >= 5) {
        // 输出内容
        $data['code'] = '0';
        $data['smscode'] = $code;
        $data['msg'] = '你发送验证码的次数达到了上限,等明天在尝试进行登陆测试!';
        $data['time'] = '120';
        echo json_encode($data);
        die;
    }

    // 生成四位数验证码,并且赋值给$code
    $code = mt_rand(0000, 9999);

    // mobile = 移动手机
    // phone = 电话

    // 查询该用户是否为注册用户。
    $db->bind('mobile', $mobile);
    $QUERY_USER_MOBILE = $db->query('select * from cycc_user where mobile = :mobile');

    // if ($mobile == '13100484705') {
    //     out_json('禁止发送到此手机号', '1', '');
    // }
    
    // 将验证码写入SESSION
    $_SESSION['code'] = $code;

    //判断其是否存在于数据库中
    if (!($QUERY_USER_MOBILE)) {
        // 添加到用户表
        $db->bind('userid', Random(6));
        $db->bind('mobile', $mobile);
        $db->bind('username', '新用户');
        $db->bind('register_ip', getip());
        $db->bind('register_time', getime());
        $INSERT_USER = $db->query('INSERT INTO cycc_user (userid,mobile,username,register_ip,register_time) VALUES (:userid,:mobile,:username,:register_ip,:register_time)');
    }

    // 短信内容
    $is_reg = $QUERY_USER_MOBILE ? '登录' : '注册';

    // 发送短信
    Send_dx_msg($mobile, '你当前正在获取'.$is_reg.'验证码,你的验证码为:'.$code);

    // 输出内容
    $data['code'] = '0';
    $data['smscode'] = $code;
    $data['msg'] = '短信验证码发送成功,请注意查收!';
    $data['time'] = '120';

    echo json_encode($data);

    // 添加到发送日志
    $db->bind('ip', getip());
    $db->bind('mobile', $mobile);
    $db->bind('code', $code);
    $db->bind('time', getime());
    $INSERT_SEND_MSG_HISTORY = $db->query('INSERT INTO cycc_send_msg_history (ip,mobile,code,time) VALUES (:ip,:mobile,:code,:time)');
break;

短信验证码发送逻辑(需要画图写清失败和成功逻辑走向):

  1. 获取参数中手机号,做数字格式校验
  2. 生成数字或字母混合验证码
  3. 发送验证码
  4. 发送成功将验证码和手机号作为 Key:Value 存入 Redis 数据库

发送短信根本原理:发短信都是把短信内容交给第三方短信服务发送。
漏洞产生原因:没有对发送次数做验证
修复思路:

  1. 发送短信前先对某个 IP 发送短信次数,今天这个手机号发送条数达到限制禁止发送。
  2. 或者计时做检查,每次发送将短信发送时间写入缓存,每次点击发送去缓存里查,只要超过 60 秒就允许发送。

短信验证码验证逻辑是:

  1. 获取参数中验证码,做格式校验
  2. 验证发送参数中验证码和 Redis 中是否一致

逗号绕过

大部分绕过逻辑和图片验证码差不多。

稍有不同的是发送验证码可以添加逗号

可以尝试向多个手机号发送短信,查了阿里云短信服务他们就是用逗号作分隔符向多个手机号发短信,这样多个手机号接收到同一条短信内容。

阿里云短信发送格式.png

如:

13412341234,13412341111

像是密码更改啊等等需要验证码验证的重点功能都可以利用。

查了下邮箱 API 也是用逗号做分隔符。

这样可能把验证码一起发送到另一个手机号,后端 API 能够同时向多个手机号发送短信,邮件也是如此。这个逻辑原本是从阿里云短信服务的文档中看到的,是正常对多个手机号发短信,但现实中很多验证码业务逻辑都是向一个手机号发送,利用此功能可能绕过限制。

空格绕过

每次发送短信前都会检查这个手机号到底发送了几次短信,每此验证码发送成功时记录此手机号到数据库里,做日志记录。

只要每次发送短信时将手机号后面加上 %20 空格或者 %0a 换行符,因为每次日志记录的手机号因为加了空格或者其他换行等空白字符是一条全新数据,所以检查结果是不满足的,就会绕过短信发送次数检查。

轰炸的时候也可以使用逗号向多个手机号发送短信快速消耗资源。

添加区号

如 13412341234 被限制

使用 086-13412341234、+86-13412341234

爆破 4 位数验证码

0000-9999

一般是六到七位数字

六位数字不能爆?和图形验证码一样,有效期过长照样爆。

并发绕过

人家限制了你一分钟发送多少条,可以尝试用

另一处就是使用多线程并发对同一个手机号发送短信,用于判断对方有没实现锁机制。

BurpSuite 自带的 Intruder 可不是真正的并发,最好用 Turbo Intruder 插件来做

IP 限制绕过

很多开发就是没考虑到应用部署方式,到底是通过网关传递用户真实 IP,还是直接获取远程连接 IP。

package baidu.login.util;

import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;

public class HttpServletUtils {
    public HttpServletUtils() {
    }

    public static String getRemoteIp(HttpServletRequest request) {
        if (request == null) {
            return null;
        } else {
            String ip = null;

            try {
                ip = StringUtils.substringBefore(request.getHeader("x-forwarded-for"), ",");
                if (StringUtils.isNotEmpty(ip) && XssStrUtils.checkIsXSS(ip)) {
                    ExceptionUtil.throwTenantException(400, "e0001", "参数中含有非法字符,请检查", new Object[0]);
                }

                if (isInvalidIp(ip)) {
                    ip = StringUtils.substringBefore(request.getHeader("X-Forwarded-For"), ",");
                }

                if (isInvalidIp(ip)) {
                    ip = request.getHeader("Proxy-Client-IP");
                }

                if (isInvalidIp(ip)) {
                    ip = request.getHeader("WL-Proxy-Client-IP");
                }

                if (isInvalidIp(ip)) {
                    ip = request.getRemoteAddr();
                }
            } catch (IllegalStateException var3) {
                ip = NetUtils.getLocalHostIp();
            }

            return ip;
        }
    }

    private static boolean isInvalidIp(String ip) {
        return StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip);
    }
}

参考资料

短信安全全面文章:https://zhuanlan.zhihu.com/p/91977462

最近更新:

发布时间:

摆哈儿龙门阵