V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mikewang
V2EX  ›  信息安全

Clash 检测工具的原理

  •  
  •   mikewang · 14 天前 · 6507 次点击

    我在 /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}}") 就能利用。
    常见的端口有 90909097。如果不是,还可以遍历 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;
    }
    

    该函数可以检测访问一个端口的耗时,并忽略失败。

    一般情况下,用户本地的端口大多处于关闭状态。对于关闭状态的端口,耗时均是差不多的。我们可以随机抽取几个端口进行检测,认为是关闭端口的耗时。

    如果 78907897 端口的访问耗时明显区分于抽选端口的耗时,我们可以推测 Clash 代理的端口是打开的。

    耗时检测为什么可行?

    上文提到过,只有 Access-Control-Allow-Origin 允许,才能进行跨域资源的访问,否则会出错。

    实际上,出错不意味着没有请求发生。浏览器首先需要发送 HTTP OPTIONS 请求,才能知道 Access-Control-Allow-Origin 的情况。这个请求称为「 Preflight request 」。跨域检测通过之后,浏览器才会再去执行脚本指定的 HTTP 请求。

    因此,即便浏览器报错拦截,实际上还是有请求发生了。这就是耗时检测的原理。

    可利用情况

    在 Windows 平台,操作系统收到 TCP RST 报文后,并不会立即认为端口关闭,而会重试 2 秒后返回错误。因此对于耗时检测,耗时两秒左右的端口是关闭的,毫秒级别耗时的端口是打开的。

    在 macOS 和 Linux 平台,情况则相反,端口关闭的耗时比端口打开的短。不过由于区分度太小,准确性较低。

    44 条回复    2024-10-06 21:56:57 +08:00
    codehz
        1
    codehz  
       13 天前
    其实还有一个保护叫做 PNA - Private Network Access ,防止处于相对更公共 ip 地址范围的网页访问更私有的网址,甚至连导航都不行
    只不过有一个小问题:它默认允许访问 127.0.0.1
    totoro625
        2
    totoro625  
       13 天前   ❤️ 1
    设置密码,改默认端口即可,最好改到其他软件的常用端口上,如 22/3389 这类

    PS:自己的配置文件自定义了端口、密码、证书加密,然后 GUI 客户端帮我删除了这些设置。
    我只想要一个便捷的 TUN 功能。
    wjx0912
        3
    wjx0912  
       13 天前
    有点慌。就直接说吧,clash 这个端口问题有木有办法解决?
    est
        4
    est  
       13 天前   ❤️ 1
    其实给子域名绑一个 ip 指向 127.0.0.1 也不跨域。
    leokun
        5
    leokun  
       13 天前
    clash 应该强制同源,仅可使用 「非浏览器客户端」打开控制端口,例如自带的 webview ,elctron 等
    Greendays
        6
    Greendays  
       13 天前
    Clash For Windows 有一个随机混合端口功能,打开后应该能降低风险
    zeusho871
        7
    zeusho871  
       13 天前 via Android
    @wjx0912 设置密码了 它只能知道你开了 clash
    lisxour
        8
    lisxour  
       13 天前
    那这应该是 GUI 工具的问题
    ohellohell
        9
    ohellohell  
       13 天前
    有没有办法避免
    @wjx0912 看起来没啥好办法啊,总归能检测到
    FengMubai
        10
    FengMubai  
       13 天前
    @Greendays 改端口不解决问题, 大可以遍历全部端口
    vvhy
        11
    vvhy  
       13 天前 via Android
    clash meta 本来就支持通过 external-ui 配置本地的 webui 资源,不知道为什么不默认禁止跨域
    不过我觉得加个密码就行了,端口响应 401 就认为是 clash 有点不靠谱
    Greendays
        12
    Greendays  
       13 天前
    @FengMubai 这样会慢很多。不过我改了端口后也确实能用楼主的工具检测出来。加密码才是最保险的。
    murmur
        13
    murmur  
       13 天前
    @leokun 只有浏览器才有这么多安全限制,自己写代码访问 http 可以构造一切头部,就包括 webview ,可以轻松关掉跨域限制,你要什么 ua ,头部,我给你构造就是了

    而且不仅是网站在扫代理,各种游戏也会扫代理,以前就爆出腾讯的国外某款射击游戏会扫描全盘,看你有没有装什么远控或者代理投屏软件
    mikewang
        14
    mikewang  
    OP
       13 天前 via iPhone   ❤️ 1
    #4 @est

    1. 访问子域名(例如 example.com 访问 sub.example.com )是跨域的。
    2. 子域名可以修改 document.domain 属性为父域名,避开跨域检测( sub.example.com 可改为 example.com )。但是这种操作已被废弃,较新的浏览器都会禁止修改 document.domain 属性。

    因此设置域名指向 127.0.0.1 的方法应该不可行。
    ohellohell
        15
    ohellohell  
       13 天前
    @ohellohell 看了下用 adblock 拦截 127.0.0.1:9090 就可以了...
    est
        16
    est  
       13 天前
    @mikewang 漏洞攻击不都是针对当前版本嘛。
    sky96111
        17
    sky96111  
       13 天前   ❤️ 1
    选择 1 ,用 FlClash 这类直接嵌入内核的软件,避免了外部通信,默认关闭外部面板访问。
    选择 2 ,用 sing-box 内核,1.10.0 版增加了 access_control_allow_origin
    cleanery
        18
    cleanery  
       13 天前
    mihomo 没有像 clash 一样 allow *,建议直接用 mihomo ,别用 clash 了
    wjx0912
        19
    wjx0912  
       13 天前   ❤️ 2
    clash verge 设置 api key 可以解决:
    ![]( )
    tabris17
        20
    tabris17  
       13 天前
    用的是 FlClash ,无法检测出来。提示:
    Access to fetch at 'http://127.0.0.1:7890/' from origin 'https://mikewang000000.github.io' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
    mikewang
        21
    mikewang  
    OP
       13 天前 via iPhone
    #18 @cleanery mihomo 是刚修好的,他们做了改进。
    文中提到的 clash 系列就包括 mihomo ,也就是 meta 。
    至于原版的 clash 核心,早删库了,应该不会复活了吧(
    yianing
        22
    yianing  
       13 天前
    @wjx0912 #3 最新的 1.18.9 支持设置 external-controller-cors 了
    74123gzy
        23
    74123gzy  
       13 天前
    页面上可以加个概率,还有检测项,告诉用户哪些行为看起来有点像 clash
    74123gzy
        24
    74123gzy  
       13 天前
    类似 whoer 那样的,
    顺便我用的 switchyomega ,完全没检测出来
    Leung818
        25
    Leung818  
       13 天前
    Mac clashX version:1.116 要咋处理呢?我在设置里把代理端口和 API 端口都改成高位的了,确实没有被检测出来,但同时我也没法在菜单栏看到订阅信息了
    zsh2517
        26
    zsh2517  
       13 天前
    有个想法但是没有测试:假设电脑的网卡 IP 是 192.168.1.2 且保证局域网是安全的,那么直接设置绑定地址为 192.168.1.2:xxxx 然后使用 web 界面时通过 192.168.1.2 的 IP 替代 127.0.0.1 是否可行?
    cybort
        27
    cybort  
       13 天前 via Android
    凡是 webui 能做的事情,其他网站也能操作
    twoz
        28
    twoz  
       13 天前 via Android
    装在路由器里面可以检测吗
    Forestar
        29
    Forestar  
       13 天前
    意思是纵使你设置密码,但也仅仅是不让配置信息暴露,通过特征分析还是知道你开了 clash 的?
    billlee
        30
    billlee  
       13 天前
    uBlock Origin 的 Block Outsider Intrusion into LAN 规则也失效了,没拦下来
    0x73346b757234
        31
    0x73346b757234  
       13 天前
    iyg429
        32
    iyg429  
       13 天前
    我用的小火箭 检测不出来
    RH
        33
    RH  
       13 天前
    @billlee 才知道还有这个,刚打开这个就检测不出来了
    RlyehHime
        34
    RlyehHime  
       13 天前
    我用的 edge+clash nya ,没检测出来
    leoking6
        35
    leoking6  
       12 天前
    联想到一个插件,Smart Referer 。arkenfox/user.js 项目推荐使用这个插件,可以有效限制跨域。strict 模式下,可将 sub-domain 也视为不同 domain ,适用于拦截类似 github.io 这种,子域名实际由不同主体持有的情况下的跨域。
    leoking6
        36
    leoking6  
       12 天前
    @leoking6 update 一下,抱歉,这个是限制 referer 的。搞错了。
    Byleth
        37
    Byleth  
       12 天前
    @0x73346b757234 这个东西一刀切拦截所有连接,会让 vite 热更新之类的功能失效,如果你在浏览器里调试的话
    fengjianxinghun
        38
    fengjianxinghun  
       11 天前 via iPhone
    webui 这种好像无法真正避免?就算了加了同源也会暴露你在用了。
    abonan
        39
    abonan  
       11 天前
    直接把回环地址改成其他的就行,只要前面是 127 开头
    xuing
        40
    xuing  
       7 天前
    @abonan 治标不治本,最好的还是 17 楼提到的通过 access_control_allow_origin 限制允许跨域的网站或者直接避免外部通信。
    xuing
        41
    xuing  
       7 天前
    @vvhy 你没有仔细看,楼主说的真的很详细了,是可以通过接口路径来判断的
    vvhy
        42
    vvhy  
       7 天前
    @xuing #41 一分钟写个 clash

    ```
    package main

    import (
    "net/http"
    "github.com/gin-gonic/gin"
    )

    func main() {
    r := gin.Default()
    r.Use(func(c *gin.Context) {
    c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
    c.Next()
    })
    r.GET("/version", func(c *gin.Context) {
    c.JSON( http.StatusOK, gin.H{"version": "1.0.0"})
    })
    r.NoRoute(func(c *gin.Context) {
    c.JSON( http.StatusUnauthorized, gin.H{"message": "Unauthorized"})
    })
    r.Run(":8000")
    }
    ```
    vvhy
        43
    vvhy  
       7 天前
    现在网页还正常吗,正经 clash 7890/9090 不加密码也显示检测不到
    mikewang
        44
    mikewang  
    OP
       7 天前
    @vvhy 合入了 https://github.com/MikeWang000000/ClashScan/pull/1 ,增大了并发数。如果原来能检测,后来检测不到了,可能是并发过大,浏览器没扛住导致的漏检
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   894 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 20:24 · PVG 04:24 · LAX 13:24 · JFK 16:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.