发表于 2024年9月29日1年前 我在 /t/1076579 给出了 Clash 检测的在线工具,有评论希望我能说明以下其中的原理。 对此比较感兴趣的,可以阅读一下本文。 1. 基于跨域缺陷的利用 首先,需要了解两个术语:「同源策略」和「跨域资源共享」。 同源策略( Same-origin Policy ) 当 B 网站的脚本,想请求 A 网站的资源时,浏览器的「同源策略」会禁止这一行为:防止 B 网站伪造您的请求,同时防止您在 A 网站上隐私的泄露。 举例: 在 www.google.com 下按 F12 ,输入 await fetch("https://bing.com") 回车,然后你会看到一条报错信息。浏览器默认阻止这一行为。 跨域资源共享( CORS ) 而这一限制有时也会过于严格,因为 A 网站和 B 网站可能是来自同一家。因此 A 网站可以设置一个列表,允许特定的网站去访问它的数据。这就是「跨域资源共享」,这个列表就是「 Access-Control-Allow-Origin 」。 举例(不好的例子): Clash / Clash Meta 核心设置了「Access-Control-Allow-Origin: *」,通配符允许了所有网站的脚本都能调用它。 假设 9090 是您的 Clash API 端口( Clash Verge 默认是 9097 )。在 www.google.com 下按 F12 ,输入 await fetch("http://127.0.0.1:9090") 回车,你会发现请求成功了。 Clash 为什么这么做? Clash 系列核心没有用户界面( GUI ),但它可以在本地运行一个 HTTP 服务,方便各种 GUI 通过 HTTP 请求调用它的接口。 其中一些 GUI 以网页形式呈现,例如「https://d.metacubex.one/」。Clash 系列核心必须开一个“后门”,也就是允许跨域资源访问,否则这些 GUI 将无法运作。 不过,这个“后门”显然开得过大了。应该增加一个配置项,只允许特定网站,而不应使用 * 通配符。 怎么利用呢? 无密码保护的情况下,任意网站调用 await fetch("http://127.0.0.1:{{port}}") 就能利用。 常见的端口有 9090 和 9097。如果不是,还可以遍历 1 - 65535 全部尝试一次。 成功利用可以获得对 Clash 核心的全部操控权限,包括获取服务器列表,修改代理规则等。 2. 基于已知路径的识别 上一节中,能利用的前提是「无密码保护」。有密码的情况下,访问接口会得到 401 的错误响应。单从 401 错误,没有证据表明用户在使用 Clash 。但我们可以通过已知路径,去识别是否为 Clash 。 Clash 的接口路径 在源码 /hub/route/server.go 中,可以获得接口的路径: /logs /traffic /memory /version ... 明显的特征是:访问这些路径,会得到 401 响应,而访问其他路径则会得到 404。 另外,不同的 Clash 版本,支持的 API 也不完全相同。通过探测支持的 API (返回 401 而不是 404),我们能进一步推断出 Clash 的版本号。 利用前提 利用的前提依然是上一节的跨域缺陷,因为如果不具备这一条,请求会直接出错失败,而不会得到 HTTP 响应码。 3. 基于代理端口的识别 前两节都是针对 API 端口的利用。我们还可以通过检测常见的代理端口(默认的 7890 和 Clash Verge 默认的 7897),判断用户是否在使用 Clash 。 普通端口的情况 对于普通的端口,不总是能碰到跨域缺陷,甚至可能都不是 HTTP 端口。对于非 HTTP 端口,不论怎么请求肯定都会是失败的。 耗时检测 参考以下 JavaScript 函数: async function portTime(port) { const st = performance.now(); try { await fetch("http://127.0.0.1:" + port); } catch (error) {} const et = performance.now(); return et - st; } 该函数可以检测访问一个端口的耗时,并忽略失败。 一般情况下,用户本地的端口大多处于关闭状态。对于关闭状态的端口,耗时均是差不多的。我们可以随机抽取几个端口进行检测,认为是关闭端口的耗时。 如果 7890 或 7897 端口的访问耗时明显区分于抽选端口的耗时,我们可以推测 Clash 代理的端口是打开的。 耗时检测为什么可行? 上文提到过,只有 Access-Control-Allow-Origin 允许,才能进行跨域资源的访问,否则会出错。 实际上,出错不意味着没有请求发生。浏览器首先需要发送 HTTP OPTIONS 请求,才能知道 Access-Control-Allow-Origin 的情况。这个请求称为「 Preflight request 」。跨域检测通过之后,浏览器才会再去执行脚本指定的 HTTP 请求。 因此,即便浏览器报错拦截,实际上还是有请求发生了。这就是耗时检测的原理。 可利用情况 在 Windows 平台,操作系统收到 TCP RST 报文后,并不会立即认为端口关闭,而会重试 2 秒后返回错误。因此对于耗时检测,耗时两秒左右的端口是关闭的,毫秒级别耗时的端口是打开的。 在 macOS 和 Linux 平台,情况则相反,端口关闭的耗时比端口打开的短。不过由于区分度太小,准确性较低。
我在 /t/1076579 给出了 Clash 检测的在线工具,有评论希望我能说明以下其中的原理。
对此比较感兴趣的,可以阅读一下本文。
1. 基于跨域缺陷的利用
首先,需要了解两个术语:「同源策略」和「跨域资源共享」。
同源策略( Same-origin Policy )
当 B 网站的脚本,想请求 A 网站的资源时,浏览器的「同源策略」会禁止这一行为:防止 B 网站伪造您的请求,同时防止您在 A 网站上隐私的泄露。
跨域资源共享( CORS )
而这一限制有时也会过于严格,因为 A 网站和 B 网站可能是来自同一家。因此 A 网站可以设置一个列表,允许特定的网站去访问它的数据。这就是「跨域资源共享」,这个列表就是「 Access-Control-Allow-Origin 」。
Clash 为什么这么做?
Clash 系列核心没有用户界面( GUI ),但它可以在本地运行一个 HTTP 服务,方便各种 GUI 通过 HTTP 请求调用它的接口。
其中一些 GUI 以网页形式呈现,例如「https://d.metacubex.one/」。Clash 系列核心必须开一个“后门”,也就是允许跨域资源访问,否则这些 GUI 将无法运作。
不过,这个“后门”显然开得过大了。应该增加一个配置项,只允许特定网站,而不应使用
*通配符。怎么利用呢?
无密码保护的情况下,任意网站调用
await fetch("http://127.0.0.1:{{port}}")就能利用。常见的端口有
9090和9097。如果不是,还可以遍历 1 - 65535 全部尝试一次。成功利用可以获得对 Clash 核心的全部操控权限,包括获取服务器列表,修改代理规则等。
2. 基于已知路径的识别
上一节中,能利用的前提是「无密码保护」。有密码的情况下,访问接口会得到
401的错误响应。单从401错误,没有证据表明用户在使用 Clash 。但我们可以通过已知路径,去识别是否为 Clash 。Clash 的接口路径
在源码
/hub/route/server.go中,可以获得接口的路径:/logs/traffic/memory/version明显的特征是:访问这些路径,会得到
401响应,而访问其他路径则会得到404。另外,不同的 Clash 版本,支持的 API 也不完全相同。通过探测支持的 API (返回
401而不是404),我们能进一步推断出 Clash 的版本号。利用前提
利用的前提依然是上一节的跨域缺陷,因为如果不具备这一条,请求会直接出错失败,而不会得到 HTTP 响应码。
3. 基于代理端口的识别
前两节都是针对 API 端口的利用。我们还可以通过检测常见的代理端口(默认的
7890和 Clash Verge 默认的7897),判断用户是否在使用 Clash 。普通端口的情况
对于普通的端口,不总是能碰到跨域缺陷,甚至可能都不是 HTTP 端口。对于非 HTTP 端口,不论怎么请求肯定都会是失败的。
耗时检测
参考以下 JavaScript 函数:
该函数可以检测访问一个端口的耗时,并忽略失败。
一般情况下,用户本地的端口大多处于关闭状态。对于关闭状态的端口,耗时均是差不多的。我们可以随机抽取几个端口进行检测,认为是关闭端口的耗时。
如果
7890或7897端口的访问耗时明显区分于抽选端口的耗时,我们可以推测 Clash 代理的端口是打开的。耗时检测为什么可行?
上文提到过,只有
Access-Control-Allow-Origin允许,才能进行跨域资源的访问,否则会出错。实际上,出错不意味着没有请求发生。浏览器首先需要发送 HTTP OPTIONS 请求,才能知道
Access-Control-Allow-Origin的情况。这个请求称为「 Preflight request 」。跨域检测通过之后,浏览器才会再去执行脚本指定的 HTTP 请求。因此,即便浏览器报错拦截,实际上还是有请求发生了。这就是耗时检测的原理。
可利用情况
在 Windows 平台,操作系统收到 TCP RST 报文后,并不会立即认为端口关闭,而会重试 2 秒后返回错误。因此对于耗时检测,耗时两秒左右的端口是关闭的,毫秒级别耗时的端口是打开的。
在 macOS 和 Linux 平台,情况则相反,端口关闭的耗时比端口打开的短。不过由于区分度太小,准确性较低。