\"合法\"的C2:深入探索VSCode开发隧道的远程访问能力"

admin 2026-10-24 15:18:51 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

\"合法\"的C2:深入探索 VS Code 开发隧道的远程访问能力

aeverj
aeverj

红队工坊

2026年10月22日 07:17
北京

去阅读

原文标题: The Accidental C2 - Exploring Dev Tunnels for Remote Access

这篇文章的开头,是在一架从曼彻斯特飞往纽约肯尼迪机场的飞机上写下的。

每次出远门,我都会从积攒的研究选题里挑一个小项目,戴上耳机,把周围的世界暂时屏蔽掉,一直折腾到飞机落地。这次也不例外。眼前有大约 7 个小时,我决定终于去研究一个惦记了很久的东西:Visual Studio Code 的开发隧道(Dev Tunnels)。

此前已经有人尝试利用开发隧道转发命令与控制(Command and Control,C2)流量。但 VS Code 自己明明就能通过这项功能在远程机器上执行 Shell 命令、传输文件。既然如此,它底层一定还有一些值得红队(Red Team)研究的能力。

通常,我不会在跨大西洋航班上启动这样的项目。原因很简单:开发隧道非常依赖稳定的网络连接,而机上花 25 英镑买来的 Wi-Fi,很多时候慢得像是靠信鸽传 IP 数据包(IP over Avian Carriers,IPoAC)。不过这次,我多了一位可以帮忙应对网络不稳定的“助手”。

■放 Bishop 出来干活

“Bishop”这个名字,来自电影《异形》中的仿生人科学官。它是我新搭建的大语言模型(Large Language Model,LLM)工作机:一台基础款 M4 Mac mini,专门用于远程运行长时间的 Claude Code、Codex 和 OpenCode 会话。

我的想法很简单:飞机网络断断续续,那就把任务交给远端的大语言模型,让它们继续分析、写代码;等网络恢复时,我再取回浓缩后的研究报告。

于是,我给出了第一条提示词:

I am creating a research-project into VS Code Dev Tunnels. Primarily the goals of this research will be:

1. Create a standalone tool which will allow me to list/add/interact with existing dev-tunnels
2. Interact with existing authentication tokens (Azure/GitHub) to view existing tunnels and interact with them

First I need to understand how they work under the hood. This will be imperative to understanding how my research will go.
The repo is at: https://github.com/microsoft/vscode.git

First take a clone of this and start exploring, looking for answers to the above.

Other resources which may be useful:
* https://github.com/microsoft/dev-tunnels.git - Dev Tunnels source

这条提示词的意思是:先不要急着动手实现工具。第一步是研究 VS Code 开发隧道的底层机制,搞清楚如何独立列出、创建和操作隧道,以及如何利用已有的 Azure 或 GitHub 身份验证令牌与隧道交互。相关源代码则从 VS Code 和微软的 Dev Tunnels 仓库入手。

出乎意料的是,GPT-5.4-Cyber 模型只用了几分钟,就给出了一份初步报告:不仅梳理了开发隧道协议的大致工作方式,还标出了对应的代码位置。

Bishop 分析 VS Code 开发隧道源代码的初步结果

感兴趣的话,可以查看[第一份研究报告] https://gist.github.com/xpn/74e0475cda48f26d653aab5b5a959ca2。[1]

后续我又让模型继续迭代,生成更多分析和代码示例。这样一来,我自己专心研究时,其他智能体(Agent)也能在后台同步推进任务。

■VS Code 开发隧道到底有几层?

VS Code 的开发隧道并不是什么新功能。在“远程资源管理器(Remote Explorer)”侧边栏里,通常就能看到它们。

VS Code 远程资源管理器中的开发隧道

乍看之下,这功能似乎很简单:不就是在两台主机之间建一条 HTTP 隧道吗?微软做隧道服务的产品也不少,这还能有多复杂?

事实证明,我想得太简单了。

微软的开发隧道由多层协议叠加而成,而且有不少地方不按常见标准出牌。想弄明白它到底如何工作,我和 Bishop 就得像剥洋葱一样,一层一层拆开。

最容易理解的方法,是从客户端连接服务器的第一步开始,沿着整个通信流程向下追踪,直到最终能够执行代码。我们先看看 VS Code 怎样找到已有的隧道。

■第 0 层:REST 管理接口

连接已有的开发隧道之前,VS Code 首先得知道有哪些服务器可用。为此,它会向一个返回 JSON 数据的 REST 接口发送 GET 请求:

GET /tunnels?includePorts=true&labels=vscode-server-launcher&allLabels=true&global=true&api-version=2023-09-27-preview HTTP/1.1
Host: global.rel.tunnels.api.visualstudio.com
Authorization: github gho_GITHUB_TOKEN_HERE
User-Agent: vscode.dev.remote-server Dev-Tunnels-Service-TypeScript-SDK/1.2.1

服务器返回可访问的隧道信息,大致如下:

HTTP/1.1 200 OK
...
{
    \"value\": [{
        \"regionName\": \"UkSouth\",
        \"value\": [{
            \"clusterId\": \"uks1\",
            \"tunnelId\": \"wild-fog-s1alk0t\",
            \"name\": \"\",
            \"description\": \"\",
            \"labels\": [\"prometheus\", \"protocolv4\", \"vscode-server-launcher\", \"_flag3\"],
            \"options\": {
                \"isGloballyAvailable\": true
            },
            \"status\": {
                \"hostConnectionCount\": 0,
                \"lastHostConnectionTime\": \"2026-04-08T16:16:57Z\",
                \"clientConnectionCount\": {
                    \"current\": 0,
                    \"limit\": 20
                },
                \"lastClientConnectionTime\": \"2026-03-31T23:11:04Z\",
                \"clientConnectionRate\": {
                    \"current\": 0
                },
                \"uploadRate\": {
                    \"periodSeconds\": 1,
                    \"resetTime\": 0,
                    \"current\": 0,
                    \"limit\": 20971520
                },
                \"downloadRate\": {
                    \"periodSeconds\": 1,
                    \"resetTime\": 0,
                    \"current\": 0,
                    \"limit\": 20971520
                },
                \"uploadTotal\": 45013860,
                \"downloadTotal\": 19000550,
                \"apiReadRate\": {
                    \"current\": 0
                },
                \"apiUpdateRate\": {
                    \"current\": 0
                }
            },
            \"endpoints\": [{
                \"hostRelayUri\": \"wss://uks1-data.rel.tunnels.api.visualstudio.com/api/v1/Host/Connect/wild-fog-s1alk0t\",
                \"clientRelayUri\": \"wss://uks1-data.rel.tunnels.api.visualstudio.com/api/v1/Client/Connect/wild-fog-s1alk0t\",
                \"id\": \"45e5e54c-1acf-41f3-96d4-c2085c0dfe35-relay\",
                \"connectionMode\": \"TunnelRelay\",
                \"hostId\": \"45e5e54c-1acf-41f3-96d4-c2085c0dfe35\",
                \"portUriFormat\": \"https://a5n51h3l-{port}.uks1.devtunnels.ms/\",
                \"tunnelUri\": \"https://a5n51h3l.uks1.devtunnels.ms/\",
                \"portSshCommandFormat\": \"ssh a5n51h3l-{port}@ssh.uks1.devtunnels.ms\",
                \"tunnelSshCommand\": \"ssh [email protected]\"
            }],
            \"ports\": [{
                \"clusterId\": \"uks1\",
                \"tunnelId\": \"wild-fog-s1alk0t\",
                \"portNumber\": 31545,
                \"protocol\": \"auto\",
                \"options\": {
                    \"isGloballyAvailable\": true
                },
                \"status\": {},
                \"portForwardingUris\": [\"https://a5n51h3l-31545.uks1.devtunnels.ms/\"],
                \"inspectionUri\": \"https://a5n51h3l-31545-inspect.uks1.devtunnels.ms/\"
            }],
            \"created\": \"2025-11-03T11:20:51.376614Z\",
            \"expiration\": \"2026-05-08T16:17:03Z\"
        }]
    }]
}

要连接其中一台正在运行的隧道服务器,我们还需要生成访问令牌。不过在此之前,先从响应中记下两个重要字段:

• clusterId:目标隧道所属的中继集群(Relay Cluster)。

• tunnelId:创建隧道时生成的随机名称,格式类似 cheeky-sausage-a123456b7。

拿到这两个信息后,就能请求新的访问令牌:

GET /tunnels/wild-fog-s1alk0t?includePorts=true&tokenScopes=connect&api-version=2023-09-27-preview HTTP/1.1
Host: CLUSTERID.rel.tunnels.api.visualstudio.com
Authorization: github gho_GITHUB_TOKEN_HERE
User-Agent: vscode.dev.remote-server Dev-Tunnels-Service-TypeScript-SDK/1.2.1

注意 URL 中的 tokenScopes=connect:我们请求的是权限范围为 connect 的令牌。响应示例如下:

HTTP/1.1 200 OK
...
{
    \"clusterId\": \"uks1\",
    \"tunnelId\": \"wild-fog-s1alk0t\",
    \"name\": \"\",
    \"description\": \"\",
    \"labels\": [\"prometheus\", \"protocolv4\", \"vscode-server-launcher\", \"_flag8\"],
    \"accessTokens\": {
        \"connect\": \"eyJhbGci[REDACTED]FxfRpNXiNGjcw\"
    },
    \"accessControl\": {
        \"entries\": []
    },
    \"options\": {
        \"isGloballyAvailable\": true
    },
    \"status\": {
        \"hostConnectionCount\": 0,
        \"lastHostConnectionTime\": \"2026-04-13T16:05:29Z\",
        \"clientConnectionCount\": {
            \"current\": 0,
            \"limit\": 20
        },
        \"lastClientConnectionTime\": \"2026-04-13T15:46:16Z\",
        \"clientConnectionRate\": {
            \"current\": 0
        },
        \"uploadRate\": {
            \"periodSeconds\": 1,
            \"resetTime\": 0,
            \"current\": 0,
            \"limit\": 20971520
        },
        \"downloadRate\": {
            \"periodSeconds\": 1,
            \"resetTime\": 0,
            \"current\": 0,
            \"limit\": 20971520
        },
        \"uploadTotal\": 6366,
        \"downloadTotal\": 15840,
        \"apiReadRate\": {
            \"current\": 0
        },
        \"apiUpdateRate\": {
            \"current\": 0
        }
    },
    \"endpoints\": [{
        \"hostRelayUri\": \"wss://uks1-data.rel.tunnels.api.visualstudio.com/api/v1/Host/Connect/wild-fog-s1alk0t\",
        \"clientRelayUri\": \"wss://uks1-data.rel.tunnels.api.visualstudio.com/api/v1/Client/Connect/wild-fog-s1alk0t\",
        \"id\": \"45e5e54c-1acf-41f3-96d4-c2085c0dfe35-relay\",
        \"connectionMode\": \"TunnelRelay\",
        \"hostId\": \"45e5e54c-1acf-41f3-96d4-c2085c0dfe35\",
        \"portUriFormat\": \"https://a5n51h3l-{port}.uks1.devtunnels.ms/\",
        \"tunnelUri\": \"https://a5n51h3l.uks1.devtunnels.ms/\",
        \"portSshCommandFormat\": \"ssh a5n51h3l-{port}@ssh.uks1.devtunnels.ms\",
        \"tunnelSshCommand\": \"ssh [email protected]\"
    }]
    ...
}

接下来,仍然要从响应里提取两个关键数据:

• connectToken:连接中继服务器时需要使用的 JWT 访问令牌(JSON Web Token,JWT)。

• clientRelayUri:客户端建立 WebSocket 连接时要访问的地址。

令牌拿到了,按理说我们可以进入下一层。不过先稍微绕个弯,聊聊最初的身份验证是怎么完成的。

■使用 GitHub 进行身份验证

如果你注意看前面的请求,就会发现:访问 REST 服务器时,Authorization 请求头里放的是 GitHub 令牌。

这个令牌来自 OAuth 2.0 授权流程。当用户第一次在 VS Code 中使用 GitHub 账户设置开发隧道时,VS Code 会发起授权。对应的 GitHub OAuth 2.0 客户端 ID 是 01ab8ac9400c4e429b23,申请的权限范围(Scope)包括 read:org 和 user:email。

值得注意的是,这个客户端 ID 还支持设备代码授权流程(Device Code Flow)。这意味着,[设备代码钓鱼(Device Code Phishing)] https://github.com/praetorian-inc/GitPhish[2] 也可能成为获取 GitHub 令牌、进而访问开发隧道的一条途径。

POST /login/device/code HTTP/1.1
Host: github.com

client_id=01ab8ac9400c4e429b23&scope=read:org,user:email

完成 OAuth 2.0 授权后,GitHub 会返回令牌:

HTTP/1.1 200 OK
...
{
    \"access_token\": \"gho_[REDACTED]\",
    \"token_type\": \"bearer\",
    \"scope\": \"read:org,user:email\"
}

这个 access_token,就是前面最初的 REST API 请求中 Authorization: github TOKENHERE 所使用的值。它还用于申请后续连接隧道所需的 connect 令牌。

■使用 Azure 进行身份验证

如果选择 Microsoft Entra ID 作为单点登录(Single Sign-On,SSO)身份提供方,流程同样遵循微软标准的 OAuth 2.0 授权机制。

这里的客户端 ID 是 aebc6443-996d-45c2-90f0-388ff96faa56,权限范围为 46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2/all。

响应里会同时出现访问令牌(Access Token)和刷新令牌(Refresh Token):

HTTP/1.1 200 OK
...
{
    \"token_type\": \"Bearer\",
    \"scope\": \"46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2/all 46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2/.default\",
    \"expires_in\": 4397,
    \"ext_expires_in\": 4397,
    \"access_token\": \"[REDACTED]\",
    \"refresh_token\": \"[REDACTED]\",
    \"id_token\": \"[REDACTED]\"
}

随后,访问令牌会以 Bearer 形式放入 Authorization 请求头。

还有一个容易忽略的细节:对于开发隧道而言,GitHub 和 Azure 实际上是两套彼此独立的隧道会话体系。通过 GitHub 身份验证创建的 VS Code 隧道,不能直接用 Entra ID 账户访问;反过来也一样。

关于身份验证,我们先记住这些,后面还会回来继续挖。

■第 1 层:WebSocket 隧道

REST 管理层搞定后,下一步是建立 WebSocket 连接。

连接目标就是之前 JSON 响应中 endpoints 对象里的 clientRelayUri。最初的 HTTP 握手请求如下:

GET /api/v1/Client/Connect/new-cat-zpzt4s0 HTTP/1.1
Authorization: tunnel [CONNECT-TOKEN-HERE]
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w17AVnNaZMHAkcZ6BrYzyg==
Host: uks1-data.rel.tunnels.api.visualstudio.com
Sec-WebSocket-Protocol: tunnel-relay-client-v2-dev, tunnel-relay-client

Sec-WebSocket-Protocol 请求头里列出了两个子协议。对于 VS Code,这里我们重点关注 tunnel-relay-client。它决定了在已建立的 WebSocket 隧道上如何使用相应的协议和身份验证方式来建立 SSH 连接。

你还会发现,之前生成的 connect JWT 令牌,此时已经出现在 Authorization 请求头里。

握手成功、WebSocket 隧道建立后,微软的中继服务器(Relay Server)就负责把客户端与服务端的 WebSocket 连接桥接起来,转发双方的数据。这样就能绕开许多直接连接时遇到的防火墙和网络地址转换等问题。

■第 2 层:SSH 连接

WebSocket 建好了,VS Code 接下来会在这条隧道内部再建立一条安全外壳协议(Secure Shell,SSH)连接。

麻烦也正是从这里开始的。

通过 VS Code 创建开发隧道服务器,或者在命令行里运行 code --tunnel 时,服务端会使用 Rust 的 [russh 库] https://github.com/microsoft/vscode-russh。可如果我们尝试用[3] Go 的 crypto/ssh,或者 Python 的 Paramiko 这样的常规 SSH 库连接,就会发现:前面的步骤看似正常,进行到一半却卡住了。

建立 SSH 连接时,客户端使用用户名 tunnel,并采用 [None 身份验证方式] https://datatracker.ietf.org/doc/html/rfc4252。乍听之下似乎很奇怪:SSH[4] 怎么能不再验证身份?原因是外层 WebSocket 已经通过 connect JWT 完成了身份验证。

但这还不够。VS Code 的隧道在密钥交换(Key Exchange,KEX)过程中,需要协商 HMAC_SHA256_ETM 消息认证码(Message Authentication Code,MAC)算法。如果没有协商成功,SSH 就会在身份验证握手阶段失败。

身份验证完成后,系统还需要建立从服务端到客户端的端口转发。这里又出现了一个不寻常的设计:发起 forwarded-tcpip 通道的竟然是客户端,而不是服务端,目标是服务器的 31545 端口。

按照常规 SSH 远程端口转发的习惯,例如 ssh -R 8080:localhost:8080,通常是在远程转发端口收到连接后,由服务端发起 forwarded-tcpip 通道。VS Code 的做法恰好与此不同,所以标准 SSH 库如果不做修改,就无法正常工作。

如果改用 russh 作为 SSH 客户端,所需的补丁可以在[这里] https://github.com/xpn/Ouroboros/blob/master/patch/russh.patch[5] 找到,并通过以下命令应用:

git apply ../patch/russh.patch

走到这里,我们终于成功在 WebSocket 隧道内部建立了 SSH 隧道。

■第 3 层:MessagePack RPC

SSH 隧道已经建立并通过身份验证,接下来终于可以交换实际消息了。

客户端与服务端之间的消息,使用 MessagePack(MsgPack)进行序列化;在其上运行的是远程过程调用(Remote Procedure Call,RPC)机制。请求结构如下:

pub struct Request<T> {
    pub id: u32,
    pub method: String,
    pub params: T
}

Bishop 在阅读源代码时,挖出了不少有意思的 RPC 方法:

• spawn_cli:通过 VS Code 服务端二进制文件调用内置 Shell 执行命令。

• spawn:直接执行命令。

• fs_read:读取文件内容。

• fs_write:写入文件内容。

• fs_connect:通过隧道连接命名管道或 Unix 套接字。

• fs_stat:获取文件元数据。

• fs_rm:删除文件。

• fs_mkdirp:创建目录。

• fs_readdir:列出目录内容。

• fs_rename:重命名文件。

• sys_kill:终止进程。

其中,我特别想验证的是 spawn:它能够在隧道服务器上执行远程命令。这个方法接收的参数如下:

pub struct SpawnParams {
    pub command: String,
    pub args: Vec<String>,
    pub cwd: Option<String>,
    pub env: HashMap<String, String>,
}

服务端返回的成功响应,同样有固定的数据结构:

pub struct SuccessResponse<T> {
    id: u32,
    result: T,
}

对于 spawn,结果部分则是:

pub struct SpawnResult {
    pub message: String,
    pub exit_code: i32,
}

为了方便逐个调用这些 RPC 方法,我和 Bishop 开发了一个名为 Ouroboros 的工具。它把这些 RPC 操作整合进了一个独立的 Rust 程序,源代码在这里:

[Ouroboros 项目] https://github.com/xpn/Ouroboros[6]

使用起来并不复杂。例如,使用 management 命令,就能列出某个账户令牌所关联的现有隧道:

[*] Starting Ouroboros Management Client
Tunnel List:
Name: sneaky-fog-s6llk1t
        Labels: [\"prometheus\", \"protocolv4\", \"vscode-server-launcher\", \"_flag3\"]
        Created: \"2025-11-03T11:20:51.376614Z\"

Name: interesting-pony-lztwqbj
        Labels: [\"serenity\", \"protocolv4\", \"vscode-server-launcher\", \"_flag8\"]
        Created: \"2026-04-13T11:54:57.1330336Z\"

Name: new-cat-zpzt4s1
        Labels: [\"bishop\", \"protocolv4\", \"vscode-server-launcher\", \"_flag8\"]
        Created: \"2026-03-20T12:59:47.5209985Z\"

Name: swift-dog-jzb5q13
        Labels: [\"romulus\", \"protocolv4\", \"vscode-server-launcher\", \"_flag8\"]
        Created: \"2026-03-31T13:06:23.5272748Z\"

找到想要操作的隧道后,就可以继续交互了:

Ouroboros 与目标开发隧道交互的演示

■Ouroboros 能在红队行动中做些什么?

工具有了,接下来就得问一个更实际的问题:它应该怎样融入我们不断扩充的红队工具箱?

用途其实不止一个。最直观的,是持久化访问(Persistence):如果一台主机已经被攻陷,就可以在上面建立新的开发隧道,让后续访问有一条现成的通道。

Ouroboros 也很适合用于横向移动(Lateral Movement):从当前系统出发,转向其他正在运行开发隧道的机器。

相关 API 凭据保存在 state.vscdb 数据库中,并通过 Electron 的 safeStorage 加密。这里值得注意的是:任何以 VS Code 相同用户身份运行的进程,都可能借助 inspect-brk 技巧提取这些令牌。

通过 inspect-brk 研究 VS Code 凭据的示例

不过,开发隧道还有第三种用途:初始访问(Initial Access)。

要理解这条思路,我们得重新回到身份验证环节。VS Code 发起登录时,会通过 OAuth 2.0 申请 Entra 访问令牌。例如,令牌请求长这样:

POST /organizations/oauth2/v2.0/token?client-request-id=ab6dc52d-ddf8-40bc-8300-f1a4bf27a4d2 HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
Host: login.microsoftonline.com
...

client_id=aebc6443-996d-45c2-90f0-388ff96faa56&
redirect_uri=http%3A%2F%2Flocalhost%3A50906&
scope=46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2%2F.default%20email%20openid%20profile%20offline_access&
code=CODE-HERE&
x-client-SKU=msal.js.node&
x-client-VER=3.8.3&
x-client-OS=darwin&
x-client-CPU=arm64&
x-ms-lib-capability=retry-after%2C%20h429&
x-client-current-telemetry=5%7C871%2C0%2C%2C%2C%7C%2C&
x-client-last-telemetry=5%7C0%7C%7C%7C0%2C0&
code_verifier=zkg1aTdQZ1k2RnUablgxQXljBWhMU0taUGl1dnVoTVpz&
grant_type=authorization_code&client_info=1

打开 [EntraScopes] https://entrascopes.com/[7] 查询,就能很快确认:这个客户端 ID 对应的是 [Visual Studio Code] https://entrascopes.com/?appId=aebc6443-996d-45c2-90f0-388ff96faa56。[8]

而请求中的资源 ID 46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2,对应的正是 Dev Tunnels Service(开发隧道服务)。

EntraScopes 中开发隧道服务的权限范围

如果再去交叉检查其他 Entra 客户端,就会发现:能够访问开发隧道服务的,并不只有 VS Code 一个应用。

也就是说,如果研究人员拿到了[这份客户端列表] https://entrascopes.com/?resource=46da2f7e-b5ef-422a-88d4-2a7f9de6a0b2[9] 中某个应用的有效用户访问令牌,就可能具备访问该用户或其组织开发隧道所需的条件。

EntraScopes 中具有开发隧道服务访问权限的客户端

但事情还能再往前走一步。

仔细看这些客户端,会发现其中有一个属于客户端 ID 家族(Family of Client IDs,FOCI):

[FOCI 客户端详情] https://entrascopes.com/?appId=872cd9fa-d31f-45e0-9eab-6e460a02d1f1[10]

FOCI 的意义在于:同一家族内的客户端之间,可能存在利用刷新令牌获取其他客户端资源访问令牌的路径。于是,假如其中某个家族成员遭到入侵,就可能进一步获得开发隧道的访问令牌。

FOCI 客户端通过 Visual Studio Legacy 访问开发隧道服务的示意图

上图只画了三个可能的客户端。但在本文发布时,EntraScopes 列出的 FOCI 客户端实际有 49 个,可以在[完整列表] https://entrascopes.com/?foci=true[11] 中查看。

举个例子:假设通过设备代码钓鱼,让目标用户完成一次 Microsoft Teams 的授权。

Microsoft Teams 设备代码授权提示

授权完成后,研究思路就是尝试将得到的刷新令牌兑换为 Visual Studio - Classic 的访问令牌。

使用刷新令牌申请其他客户端访问令牌的请求示例

但这里必须说清楚实验结果:在当时的测试中,这个方法对 Microsoft 个人账户(也称 Live 账户)有效;而 Entra ID 租户似乎会拒绝将 Visual Studio - Classic 视为 FOCI 客户端。 它可能已经被移出 FOCI 列表。不能把个人账户上的测试结果,直接推广到企业租户。

考虑到这种可能性,我又转向了另一类客户端:BroCI(NAA)。关于这种嵌套应用身份验证(Nested App Authentication)机制,我的 SpecterOps 同事 Hope Walker 在[这篇文章] https://specterops.io/blog/2025/08/13/going-for-brokering-offensive-walkthrough-for-nested-app-authentication/[12] 中有详细介绍。BroCI 同样扩大了潜在攻击面。

继续查下去,我们找到了 AzurePortal Console App。

EntraScopes 中 AzurePortal Console App 的应用信息

AzurePortal Console App 是一个嵌套应用(Nested Application)。从它的重定向 URI(Redirect URI)可以看出,它依赖 Azure Portal 应用充当代理,签发新的访问令牌。

这意味着,如果我们已经拥有某个用户的 Azure Portal 会话,就可能沿着这条关系链,访问该用户或组织的开发隧道。

Azure Portal 经由 BroCI 访问开发隧道服务的关系示意图

■双面性:这项研究究竟是谁完成的?

说实话,刚开始做这个项目时,我一度觉得,最后写出来的可能只是一篇“快看,大语言模型居然能做这么多事!”的文章。

直到我开始梳理攻击路径,才突然想到一个问题:你现在读到的这些研究成果,到底有多少应该算在 Bishop 头上?我想认真谈谈这件事,因为我知道,很多人也在思考同样的问题。

首先,我从开始使用大语言模型起,就给自己定下了一条原则:绝不会把大语言模型生成的成果冒充成自己独立完成的工作;凡是模型提供过帮助,都应该明确说明。

所以,这次项目的分工可以说得很清楚:Bishop 负责梳理开发隧道协议,并编写 russh 库所需的补丁;我则亲手写出了 Ouroboros 的第一个版本([这里有当时的记录] https://x.com/\xpn\/status/2044449288218935731),并亲自探索攻击路径,把整个研究组织成面向红队的技术方案。[13]

而在你读到这篇文章之前,很可能也有模型参与过文字校对。当然,还有我们出色的技术编辑 [Sarah] https://www.linkedin.com/in/sarah-miles-81ab6a41/。正是她帮忙把我们写出来的一大堆零散文字,整理成读者能顺畅读下去的文章。[14]

在安全研究圈里,我们早就有一种很好的传统:不管是直接使用别人的成果,还是从别人的工作中获得灵感,都应该注明来源。

大语言模型显然也站在无数前人的工作之上。你可以试试,让模型针对自己正在研究、训练数据里又不太可能有现成答案的新课题提出原创思路,就会体会到数据不足时的差别。

过去 40 年里,无数黑客把自己的知识和经验免费分享到了互联网上。模型所表现出来的能力,很大程度上建立在这些积累之上。如果把这些人的集体智慧全部算成自己的功劳,我总觉得哪里不对。

但换个角度说,我们本来就是站在巨人的肩膀上继续前进。如果你能在大语言模型生成的研究基础上加入真正有价值的原创判断,那同样值得肯定。 前提是:模型提供过的帮助,也应该得到承认。

可这篇文章还是让我冒出了几个问题。

第一个问题是:这项研究,真的是我完成的吗?

把一个项目交给大语言模型,然后看着它拿出惊人的结果,自己却不用再熬上好几个痛苦的夜晚——这种感觉直到现在仍有点奇怪。

但从职业角度看,我的答案是:这并不重要!

我的意思是,价值并不取决于究竟是谁敲下了代码。最初,我希望有一个工具,能让其他 SpecterOps 同事在安全评估中使用。现在工具已经做出来了,而且确实能用,满足了我受雇完成的工作目标。更重要的是,它所花的时间比过去少得多,其他人也能更早用上。

从个人角度看,我会不会觉得自己被剥夺了发现新技术、亲自探索的乐趣?答案同样是:不会。

推动我继续做安全研究的,从来不是“我亲手写了多少行代码”,而是拆开一项技术、真正理解它的工作原理,然后琢磨怎样以攻击者的思路重新利用它。在这个项目结束时,我获得的满足感和过去亲手完成的研究并没有什么不同。

当然,我完全可以把 Claude 或 GPT 的输出直接贴进博客,写完收工。但我的目标始终没变。所以我还是坐下来,仔细研究了模型给出的分析结果,并亲手写出了 Ouroboros 的第一版原型。只有这样,我才能真正理解每一部分是怎样拼在一起的。

最终,我做出了一个非常早期的概念验证(Proof of Concept,PoC)版本,并把它分享到了内部:

作者早期向内部分享 Ouroboros 原型的记录

等我确认自己已经学到了想学的东西,并且把新的技术手法整理进 Notion 作战手册之后,剩下的工作其实就是把其他 RPC 命令接进工具。

而这种相对明确、重复性较高的工程任务,恰恰是大语言模型擅长的。我于是让 Bishop 接手,把工具完善成最终版本。这也让它比过去更快地在内部投入使用。

不过,还有一件事让我重新思考 Bishop 在这个项目中的贡献,那就是前面 Entra ID 的 FOCI/BroCI 部分。

我在给模型布置研究任务时,已经明确告诉它:我要做的是一款用于红队行动的 C2 工具。但在整个过程中,GPT-5.4-Cyber从未主动联想到:GitHub OAuth 身份验证流程里可以怎样利用设备代码钓鱼。同样,它也没有指出可以通过 FOCI,把刷新令牌转换为能够访问现有开发隧道的访问令牌。

这些联系不是模型告诉我的,而是我自己想起来的。

2024 年,我曾和一群 SpecterOps 同事一起,参加过 [Dirk-jan] https://x.com/\_dirkjan[15] 为期一周的 Entra 培训。关于 FOCI 的思路,正是从那段经历里冒出来的。

BroCI 也是如此。和模型交互时,它同样没有主动提起 BroCI。真正让我想到这一点的,是去年年初芝加哥的一场内部黑客马拉松。当时 [Chris] https://x.com/\_Mayyhem、[Hope[16]] https://x.com/Icemoonhsv、[Darrius[17]] https://x.com/rbnroot[18] 和 [Costa] https://x.com/sn8kesec[19] 介绍过 BroCI 相关研究。

如果我直接问模型这些技术之间能不能关联起来,它能不能给出答案?当然能。

如果我在提示词里逐步把它引向这个方向,它能不能找到相同的攻击路径?当然也能。

但关键就在这里:当时的我并不知道研究最后会走向哪里,所以根本不会提出那些问题。

这项技术会不会还有其他可利用的路径,而且模型其实有能力找到?我相信很可能有。但要把它们挖出来,你得先提出正确的问题;而提出正确的问题,又需要足够深厚的领域知识和实践经验。至少在这次研究里,模型并没有自动把这些线索串起来。

换句话说……

大语言模型可没有在某个周一早晨醒来,突然决定大开杀戒。真正动这个念头的人,是我!

■相关的先前研究

写这篇文章时,我还专门去找了找,看是否有人已经研究并公开记录过这套协议。

4 月初,Karpagarajan Vikkii 和 Amanda Rousseau 在 [Straiker] https://www.straiker.ai/[20] 上发表了一篇非常精彩的 Cursor 研究文章:

[NomShub:Cursor 远程隧道与沙箱逃逸研究] https://www.straiker.ai/blog/nomshub-cursor-remote-tunneling-sandbox-breakout[21]

这篇文章不仅清楚介绍了隧道协议规范,还重点分析了相关 RPC 命令。我想在这里明确感谢并认可他们的研究成果。

最初看到别人已经[发表了相关技术文章] https://www.straiker.ai/blog/nomshub-cursor-remote-tunneling-sandbox-breakout[21] 时,我甚至打算取消自己的博客计划。但仔细比较之后,我认为两项研究最终走向的应用场景不同:他们的研究和我这个面向红队的工具及技术分析,依然各有价值。

还有一点值得特别注意:虽然他们研究的是 Cursor,但 Cursor 本身就是基于 VS Code 分叉开发的,也使用相同的开发隧道协议。

因此,本文讨论的内容,适用范围远不止 VS Code。

祝各位黑客玩得开心,h4xx0rz!

如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号

在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!

\"合法\"的C2:深入探索VSCode开发隧道的远程访问能力" 网络安全文章

\"合法\"的C2:深入探索VSCode开发隧道的远程访问能力"

本文探讨将VSCode开发隧道用作C2通信通道的可行性。作者利用LLM助手Bishop分析微软开发隧道的底层协议,揭示其由多层协议叠加而成,可通过REST接口列出和操作隧道,并需Azure或GitHub认证令牌交互。文章详细介绍了隧道发现、连接及访问令牌生成的技术细节,为红队研究提供了可操作的技术路径。
多值参数污染与大小写绕过导致账号接管" 网络安全文章

多值参数污染与大小写绕过导致账号接管"

本文分析了一个账号接管漏洞,源于邮箱更新接口的多值参数污染和大小写绕过缺陷。攻击者可构造多值参数或大小写变体绕过唯一性校验,将账号邮箱覆盖为受害者邮箱,进而通过忘记密码流程接管账号。文章提供了详细测试流程和危害说明,强调合法授权测试。
工具|Cairn" 网络安全文章

工具|Cairn"

Cairn是一款通用状态空间搜索引擎,具备AI渗透测试功能,项目托管于GitHub。文档提供工具简介与项目地址,并附免责声明,强调仅供安全研究与学习使用,使用者需自行承担法律责任。该工具定位明确,但文档内容较为简略,未提供具体技术细节或使用指南。
评论:0   参与:  0