最新更新,svg-captcha
即将发布3.0,修复该漏洞
详情issue:https://github.com/produck/sv...
以下原文
可能有部分nodejs开发者因为安装图形库很麻烦,都用svg-captcha来生成图形验证码
svg-captcha
: https://www.npmjs.com/package...
它有不少优点,具体看官方文档。然而它最大的一个缺点就是,太容易被破解了,是我刚破解的(其实是我在2019年,第一次知道它后没多久,无意中发现破解方法的,直到现在才有空提交代码)
破解指的是,很容易被机器识别,识别率达100%,并且不需要任何机器学习有关的知识,更不需要任何图形识别库,你可能根本想象不到破解方法有多简单。
如果没有兴趣看下面那么长,可以直接看识别代码:
https://github.com/haua/svg-c...
发现流程
刚接触svg-captcha
的时候就在想,它到底为防止识别做了哪些操作,然后仔细对比相同字母,发现每次生成相同字母的轮廓,不一致的地方相当多:
这样看来,即使是相同字母,它的svg path,也是完全不一样的,看来如果要破解,确实只能先把它转为位图,再做图像识别了。
然而,有一次无意中生成了两个首字母相同的验证码,看到了如下画面:
图中上下两行分别是两次生成的svg,
(如果不清楚svg path是啥,可以去看看svg的组成,svg可以看作是一个html标签,path是它的一个重要属性)
那既然如此,是不是可以根据svg path长度来区分它是哪个字母??
于是用js写了个脚本,遍历调用svg-captcha
来生成验证码,把每个验证码中的字母path长度做个统计,一共只有26个字母+10个数字,结果如下
{
'0': { '2382': 10819, '2580': 3769 },
'1': { '998': 10861, '1081': 3621 },
'2': { '2546': 11012, '2758': 3676 },
'3': { '3878': 10913, '4201': 3599 },
'4': { '2140': 10985, '2318': 3674 },
'5': { '2606': 10925, '2823': 3623 },
'6': { '2632': 10834, '2851': 3615 },
'7': { '2042': 10956, '2212': 3636 },
'8': { '3414': 10742, '3698': 3638 },
'9': { '2800': 5343, '3033': 1811 },
A: { '1840': 3618, '1844': 1825, '1993': 1885 },
B: { '3054': 11034, '3308': 3710 },
C: { '2198': 8820, '2199': 1536, '2200': 538, '2201': 92, '2202': 4, '2381': 3579 },
D: { '1996': 10745, '2162': 3651 },
E: { '2246': 10939, '2433': 3569 },
F: { '1754': 10911, '1900': 3689 },
G: { '3266': 10989, '3538': 3579 },
H: { '1922': 7220, '1928': 3663, '2082': 3668 },
I: { '986': 11125, '1068': 3628 },
J: { '1610': 10965, '1744': 3557 },
K: { '1706': 7291, '1709': 3534, '1848': 3656 },
L: { '1274': 10949, '1380': 3722 },
M: { '2279': 3560, '2282': 3675, '2321': 3666, '2466': 3649 },
N: { '1598': 7429, '1614': 30, '1615': 379, '1616': 1312, '1617': 1728, '1618': 330, '1731': 3553 },
O: { '2164': 10695, '2344': 3602 },
P: { '1960': 10949, '2123': 3630 },
Q: { '3244': 7162, '3254': 3616, '3514': 3676 },
R: { '2104': 7284, '2107': 3580, '2279': 3682 },
S: { '3038': 10732, '3291': 3631 },
T: { '1478': 11016, '1601': 3623 },
U: { '2294': 7360, '2301': 3585, '2485': 3640 },
V: { '1298': 7251, '1311': 3589, '1406': 3666 },
W: { '2310': 3664, '2318': 3644, '2345': 2516, '2346': 1114, '2503': 3624 },
X: { '1598': 7382, '1604': 3635, '1731': 3614 },
Y: { '1130': 7310, '1134': 3611, '1224': 3597 },
Z: { '1850': 7223, '1853': 3620, '2004': 3595 },
a: { '2332': 10944, '2526': 3711 },
b: { '2380': 10844, '2578': 3617 },
c: { '2498': 10924, '2706': 3720 },
d: { '2272': 10828, '2461': 3601 },
e: { '2501': 10802, '2709': 3558 },
f: { '2210': 11041, '2394': 3612 },
g: { '3160': 11074, '3423': 3708 },
h: { '1886': 10731, '2043': 3546 },
i: { '1360': 11038, '1473': 3605 },
j: { '2080': 10842, '2253': 3581 },
k: { '1634': 7341, '1637': 3601, '1770': 3664 },
l: { '986': 10975, '1068': 3589 },
m: { '3663': 7200, '3667': 3638, '3968': 3747 },
n: { '2198': 10825, '2381': 3668 },
o: { '2260': 11180, '2448': 3740 },
p: { '2464': 10860, '2669': 3653 },
q: { '2512': 10844, '2721': 3641 },
r: { '1491': 11032, '1615': 3659 },
s: { '2366': 10812, '2563': 3604 },
t: { '1694': 10790, '1835': 3615 },
u: { '1838': 10953, '1991': 3656 },
v: { '1082': 10959, '1172': 3543 },
w: { '2018': 7242, '2035': 3735, '2183': 3672 },
x: { '1610': 7350, '1613': 3706, '1744': 3762 },
y: { '1274': 10830, '1380': 3490 },
z: { '1694': 11224, '1835': 3701 }
}
每个字母的值都是一个对象,这个对象的key是path长度,value是这个长度出现的次数,因为还想看看每个长度出现的概率,所以跑了几十万次,也为了防止有些长度没出现过。
可以看到,每个字母的path长度,也就那几种。这样看来,svg-captcha
也太容易破解了吧!
根据以上的统计,有15个字母的path长度存在相同的情况,所以用这个方法的准确率应该不到50%
继续看看那些有相同path长度的字母,发现它们还有很大的不同,比如I
和l
都有相同的path长度(986),但是对比一下:
左边是I
,右边是l
,可以看到l
的最上面,要比I
要高一点,虽然直接根据这个特征判断I
还是l
,似乎很没说服力,但是试了生成几万个I
和l
,这个的差别都是一样的,这样的话这个特征肯定能拿来用了。
其它字母也差不多是这种细微,但可靠的特征,最终做到了100%准确识别率。
如此看来,svg-captcha
也只是做到了看起来比位图验证码要难识别,实际上它更容易识别,识别方法也更原始。
建议
如果你还是想使用svg-captcha
,那我建议可以多准备几套字体,写个逻辑,让它每小时换一种字体,这样生成的字母svg path会完全不一样。