GitHub Trending 这两天有个安全工具蹿得挺凶:sherlock,Python 写的,输入一个用户名,它去 400 多个社交平台并发查一遍这个用户名有没有被注册,把命中的链接列出来。我扫到它的时候总星数 10.7 万,单日涨了 4000 多——这个数跟腾讯云社区日榜的 10.7 万、GhTrends 统计的量级都对得上,掘金热榜的 GitHub 日榜上它也连着挂了两天。sherlock 不是新面孔,Debian 和 Mageia 的软件源里都收着它,这波突然翻红,我没查到具体由头,推测跟最近隐私事件扎堆有关系。
工具两头用:安全人员做渗透前的信息收集,普通人自查——你的常用用户名被哪些平台占了。我感兴趣的是另一个问题:它报出来的"命中",到底有多可信?这个问题热榜上没人答,README 也不会告诉你,因为答案不好看。 正好手头有台 1 核 2GB 的 Ubuntu 小机,uv 是现成的,装起来两分钟。
先跑一个真实的名字
$ uv venv sherlock_venv && uv pip install sherlock-project
$ sherlock --version
Sherlock v0.16.2
实测名我选了 torvalds——Linus 的用户名,GitHub、Launchpad 这些平台上的占用是公开可查的,等于自带标准答案。并发跑完 481 个站点的配置,耗时 3 分 16 秒,报了 64 个命中:
[+] GitHub: https://www.github.com/torvalds
[+] Launchpad: https://launchpad.net/~torvalds
[+] Gitee: https://gitee.com/torvalds
[+] last.fm: https://last.fm/user/torvalds
[+] osu!: https://osu.ppy.sh/users/torvalds
...共 64 条
前几条点开都对。要不是后面多看了一眼,我可能就存档收工了。
一个连用户名都不带的"命中"
64 条里有一条是这样的:
[+] Kvinneguiden: https://forum.kvinneguiden.no
Kvinneguiden 是个挪威论坛。不对劲的是:这个链接是站点主页,URL 里根本没有用户名。指向主页的"命中",意味着判定环节根本没拿到带用户名的页面。
我拿一个随机字符串 qzxvbnm7421ktest 做对照——这种名字不可能在任何平台被注册过,理想情况应该 0 命中。整轮跑下来确实是 0 命中,一个站都没报。但单独对 Kvinneguiden 重跑一次随机名:
$ sherlock qzxvbnm7421ktest --site Kvinneguiden
[+] Kvinneguiden: https://forum.kvinneguiden.no
随机名也"存在"。实锤了,这个站点是配置性误报:不管你查什么名字,它都会报命中。
为什么?去翻了它的判定配置(包里的 resources/data.json,sherlock 对 481 个站点各维护一条查询规则)。Kvinneguiden 用的是 message 模式:请求一个 AJAX 探针地址,约定响应里包含 {"result":"ok"} 这个字符串就代表"用户名不存在"。sherlock 的逻辑是反过来的——没看到这句约定文案,就判"存在"。问题在于探针一旦返回别的东西——被 Cloudflare 拦截的 403 页面、登录墙、维护公告,响应里当然也没有那句 JSON,于是拦截页本身被当成了"账号存在"的证据。我 curl 它的主页,返回的就是 403。这类判定建立在一个脆弱假设上:探针要么正常返回约定文案,要么干脆别返回——现实里中间态一堆。
误报还是真占用,得看探针打的哪里
64 条命中里还有一批我当时差点一起打成"误报"的。Duolingo 我直接 curl 它的个人主页,随机名和 torvalds 都返回 200——典型的 SPA 泛 200,按经验该归为误报。但看配置发现不对:sherlock 没有打主页,它的 urlProbe 指向 Duolingo 的官方用户查询接口,判定文案是 {"users":[]}(空数组=不存在)。拿这个接口直接验证:
$ curl "https://www.duolingo.com/2017-06-30/users?username=torvalds"
{"users":[{"id":14226320,...}]}
$ curl "https://www.duolingo.com/2017-06-30/users?username=qzxvbnm7421ktest"
{"users":[]}
torvalds 在 Duolingo 确实有一个真实用户,随机名返回空数组。这条是真命中。同一个站,探针打主页就是误报现场,打 API 就准——sherlock 对部分站点做的这层 urlProbe 设计有效,但 481 条规则的质量,完全看每个维护者的认真程度。
结果会跟着网络状态抖
Xbox Gamertag 这条暴露了另一个问题。它是 status_code 模式:HTTP 状态码落在约定区间就判存在。torvalds 那轮它报了命中,但等我去复测,站点反爬直接回我 403。这条命中的产生条件其实是"当时响应恰好是 2xx"——而它查的是个第三方查询站的搜索页,搜索页对任何关键词都可能是 200,跟 Xbox 上有没有这个 gamertag 没有必然关系。
抖动不止发生在单站。同一个随机名,整轮跑是 0 命中,单站重跑 Kvinneguiden 就中——两轮之间隔了几分钟,唯一的区别是整轮跑时几百个站点并发,那个探针可能恰好超时或返回慢了,没来得及给出一个"可误报"的响应。所以它的输出不是确定性的:同一台机器、同一个用户名,跑两遍的结果可以不一样。README 对此只字未提。
64 个命中,最后我信了几个
逐条按上面的方法过了一遍能验证的,大致分三类:GitHub、Launchpad、WordPress、last.fm、osu! 这类主页直连的,点开就能确认用户名真实被占用,占大头;Duolingo 这种走 API 探针的,验证后为真,数量不多;Kvinneguiden 这种配置性误报,加上 Xbox Gamertag 这种反爬抖动的,两只手数得过来但都挺隐蔽。还有个语义上的坑值得说:RuneScape 报了命中,但那是游戏里某个玩家占用了这个 ID,跟 Linus 本人毫无关系——sherlock 查的从头到尾是"用户名被谁占了",不是"这个人是不是你要找的人",拿它做背调的人最好清楚这个区别。
回到自查这个用途,我的结论是:拿它扫一遍自己的常用用户名,第一轮结果当筛子用是合格的——真实占用基本都在里面;但任何一条要在别处引用之前,先点开链接看两样东西:URL 里带不带你的用户名,判定是哪种模式。前者不带你就要小心,后者可以在 data.json 里查到,status_code 打在搜索页上的、message 探针返回异常页的,都值得复核。工具本体逻辑没毛病,坏在 481 条规则里总有几条跟站点的反爬策略不同步——这也解释了为什么它必须把规则做成数据文件:站点一改版,改一行 JSON 就能修,前提是你得知道自己那条命中是怎么来的。