用CloudFlareWorkers实现有访问控制的Payload投递

admin 2025-12-26 01:43:20 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档介绍了利用CloudFlareWorkers实现有条件访问的Payload投递技术。通过中间件校验Token,Worker内部拉取文件返回,实现无源站暴露与即时密钥轮换。内容涵盖代码实现、部署流程及防御检测策略,建议红队利用边缘节点隐蔽性,同时提醒蓝队重点监控非浏览器进程对pages.dev的异常访问与二进制下载行为。 综合评分: 93 文章分类: 红队,实战经验


cover_image

用 CloudFlare Workers 实现有访问控制的 Payload 投递

Andy Gill

securitainment

2025年12月25日 10:24 中国香港

有时你需要分发任意内容 (无论是文件、二进制、图片或其他内容),同时还要能用同样“任意”的条件来限制访问。用 Apache 服务器配合 mod_rewrite规则当然能做到;但你也可以用 CloudFlare Workers,构建一种高可用 (只要 CF 不把全网搞崩;最近两次都中招,2/2!)、并且可快速更新的交付方式。类似做法的叫法很多,但我更喜欢 “Conditional Access Payload Delivery (CAPD)” 这个名字。

为什么选择 CloudFlare Workers?

CloudFlare Workers 运行在全球大量边缘节点上,不需要源站;你的 payload 会从距离请求方最近的节点被交付。访问密钥轮换几乎是即时的 (改环境变量,无需重新部署),没有服务器可供指纹识别,还能顺带获得 CloudFlare 的 DDoS 防护。

我个人很喜欢 serverless 架构,用它来做 payload 交付也很自然。你可能已经熟悉两种“条件访问”的做法:用 Apache mod_rewrite规则,或者在你的 C2 redirector 上做一个简单的鉴权端点。两者都能用,也各有利弊。把交付逻辑放到“背后没有服务器”的位置,可以进一步收紧基础设施暴露面、降低维护成本,并带来更好的地理位置灵活性。

缺点也很现实:你是在把 payload 交给 CloudFlare 托管;如果你不熟悉 Workers,会有学习成本;免费套餐存在请求额度限制 (100k/day),在高频操作中可能成为瓶颈。对大多数红队交付场景来说,可用性与操作灵活性往往更重要。

前置条件

你需要使用 CloudFlare 的 CLI 工具 wrangler 来部署 CloudFlare Pages 内容并集成 Worker 函数。如果你希望自动化部署函数,就不能使用直接上传 (direct upload) 的方式。Wrangler 是一个 npm包;如果你还没有安装 Node 版本管理器,我推荐使用 Volta。

curl https://get.volta.sh | bash
volta install node
node --version

或者如果你在 Windows 上,可以用 winget 安装:

winget install Volta.Volta
volta install node
node --version

项目结构

创建一个新的工作目录并安装 wrangler。

mkdir capd && cd capd
npm install wrangler --save-dev

为了存放 Worker 函数代码,你需要一个名为 functions的子目录,然后在该目录中创建一个 TypeScript 文件。这里我用一个名为 _middleware.ts的例子 (你会在后文看到完整代码)。

mkdir functions
touch functions/_middleware.ts

在项目根目录中,放置你希望对外提供的任何文件、二进制等。如果你不希望这些文件能被穷举或常见路径扫描发现,就应该给文件起一个很长且不可猜测的名字。一个简单的建议是:使用文件的 hash 摘要作为文件名。

请求流程

在进入代码之前,先看一下整体流程:

客户端不会直接访问静态资源。Worker 会拦截每一次请求、校验 token,只有通过后才会在内部从 Pages 拉取资源。鉴权失败会返回一个通用错误,不泄露“资源是否存在”的任何迹象。为了更直观地展示多 token 的 CAPD 用法,再看一张图:

中间件函数

编辑你的 TypeScript 文件并加入下面的代码。你需要把各个常量改成你自己的值:PRESHARED_AUTH_HEADER_KEY是 HTTP header 名称 (由入站请求携带);PRESHARED_AUTH_HEADER_VALUE是对应的值;PAYLOAD_PATH是你希望交付文件的完整 URL。脚本会用它直接拉取资源;由于这个请求由 Worker 自己发起,因此可以绕过对外的“门禁”。

const PRESHARED_AUTH_HEADER_KEY = "Authorization";
const PRESHARED_AUTH_HEADER_VALUE = "Basic 6288f2e08c599e01fd28566e7aa38d54f37439c8a5a6c46cf08a2b8bdfad0b8a";
const PAYLOAD_PATH = "https://cloudflare_project_name.pages.dev/random_file_name"

const servePayload = async (context) => {
  if (context.request.headers.has(PRESHARED_AUTH_HEADER_KEY)) {
    const auth_token = context.request.headers.get(PRESHARED_AUTH_HEADER_KEY);
    if (PRESHARED_AUTH_HEADER_VALUE == auth_token) {
      try {
        const asset = await context.env.ASSETS.fetch(PAYLOAD_PATH);

        if (!asset.ok) {
          // Asset missing or inaccessible - fail silently
          return new Response("Service Unavailable", { status: 503 });
        }

        const newResponse = new Response(asset.body, asset);
        newResponse.headers.set("Cache-Control", "no-store");
        return newResponse;
      } catch (err) {
        // Don't leak stack traces or error details
        return new Response("Service Unavailable", { status: 503 });
      }
    }
  }

  return new Response("Service Unavailable", {
    status: 503
  });
};

export const onRequest = [servePayload];

这里用 try/catch的目的,是确保“文件缺失、网络抖动或其他异常”与“无效 token”返回完全一致的通用 503;既不泄露堆栈信息,也不给攻击者任何可用来做指纹识别的差异信号。

只要请求通过 header 校验,这个函数就会返回 PAYLOAD_PATH对应的内容。客户端请求的 URL 路径是什么并不重要,因为响应是由固定常量构造出来的。如果你希望根据不同条件返回不同文件,可以使用 URL 的路径参数来选择文件;或者使用多个不同的 header 值,并结合 if条件,根据 header 中的值返回对应文件。

部署

当你准备好脚本和文件后,到 CloudFlare 创建一个新的 Pages 实例。拿到 project ID (也就是你给该 Pages 实例起的名字)。给 Pages 实例命名时,我建议尽量低调一些,别太显眼。

cd capd
ls
./random_file_name ./functions/_middleware.ts
npx wrangler pages deploy . --project-name <cloudflare_project_id>

这会把静态文件推送上去,同时把函数代码部署成一个 Worker。

验证

部署完成后,你可以用 cURL 测试。如果函数工作正常,带上正确的 header 值会得到 200 OK

curl -I -H "Authorization: Basic 6288f2e08c599e01fd28566e7aa38d54f37439c8a5a6c46cf08a2b8bdfad0b8a" https://cloudflare_project_name.pages.dev/

HTTP/2 200

任何不包含有效 header 的请求都会返回 503 Service Unavailable

curl -I https://cloudflare_project_name.pages.dev/

HTTP/2 503

这种“门禁式”方法有个很实用的特点:你可以在不修改请求端 (客户端或二进制) 的前提下,为某个 token 随时替换实际交付的内容。

变体与扩展

用环境变量替代硬编码密钥

在真实部署中,你不应该把 token 提交到源码里。可以改用 Pages/Workers 的环境变量:

const key = "Authorization";

export const onRequest = async (context) => {
&nbsp; const expected = context.env.PRESHARED_AUTH_HEADER_VALUE;
&nbsp; const provided = context.request.headers.get(key) || "";

&nbsp; if (provided === expected) {
&nbsp; &nbsp; // serve payload
&nbsp; }

&nbsp; return new Response("Not Found", { status: 404 });
};

这样你就能在不重新部署整个项目的情况下,几乎即时完成 token 轮换。

时间安全比较

最基础的 ==比较存在 timing attack 风险:攻击者可能通过测量响应时间差异,统计性地推断出正确 token。实际在网络环境中这往往很难利用,但如果你想加固实现,可以这样做:

async function safeCompare(a: string, b: string): Promise<boolean> {
&nbsp; const encoder = new TextEncoder();
&nbsp; const aBytes = encoder.encode(a);
&nbsp; const bBytes = encoder.encode(b);

&nbsp; if (aBytes.length !== bBytes.length) {
&nbsp; &nbsp; // Compare against self to maintain constant time
&nbsp; &nbsp; await crypto.subtle.timingSafeEqual(aBytes, aBytes);
&nbsp; &nbsp; return false;
&nbsp; }

&nbsp; return crypto.subtle.timingSafeEqual(aBytes, bBytes);
}

// Usage in your handler
const provided = context.request.headers.get(key) || "";
if (await safeCompare(provided, expected)) {
&nbsp; // serve payload
}

这使用了 Workers 中可用的 Web Crypto API。长度检查的分支仍然调用 timingSafeEqual,以避免通过时间差泄露长度信息。

按活动或按 payload 的 token

如果你需要多个 payload,或希望在不同 engagement 之间隔离 token,可以把多个 header 值映射到不同文件:

const TOKENS = {
&nbsp; "Basic tokenA": "https://…/payloadA",
&nbsp; "Basic tokenB": "https://…/payloadB"
};

这样你可以清晰地区分不同操作边界,并按 operation 维度下线访问权限。一个很好的用例是:当你投递 initial access 的 payload 且希望降低暴露风险时,可以把“当前正在交付的内容”实时切换为远程加载 shellcode 等形式,并继续使用条件访问来控制交付。

调整失败响应

返回 503是可行的,但根据环境你可能更希望用更“普通”的响应:

  • 404 Not Found,用于伪装成不存在的路径
  • 200 OK,返回一个无害的 HTML 页面
  • 或者轮换几种“看起来正常”的响应

这能避免形成一个稳定且容易被蓝队指纹识别的特征。

控制文件行为

如果你希望 payload 被直接下载而不是在浏览器中渲染:

newResponse.headers.set("Content-Disposition", "attachment; filename=update.bin");
newResponse.headers.set("X-Content-Type-Options", "nosniff");

当返回二进制或不应以内联方式展示的工具时,这会很有用。

使用 Wrangler 本地测试

在部署前,你可以先在本地测试 header 保护逻辑:

npx wrangler pages dev .

这样可以验证逻辑而无需推送到 CloudFlare,避免在你的 campaign 过早暴露相关基础设施或工具痕迹。

最小化日志的收益

Workers 可以记录哪些客户端成功拉取了 payload,这对冲突消解 (deconfliction)、时间线分析,或追踪“哪个 token 真的被用过”都很有帮助。你可以把日志保持在最小化水平,避免存储敏感内容;必要时只记录时间戳与请求元数据即可。

蓝队检测视角

CAPD Workers 会在网络遥测、端点日志和代理数据中留下可供防守方追踪的模式。理解这些指标对双方都有价值:红队可以据此改进作业手法 (tradecraft),蓝队也能在野外发现类似行为时知道该看什么。

先说明一下:我日常并不是检测工程师或蓝队,所以下面的一些检测思路可能并不完全准确;但我希望这些内容能给比我更聪明的人一些启发,帮助做出更好的检测。

网络层指标

异常的 Header 模式

自定义鉴权 header 会在代理日志里非常显眼。大多数合法的 CloudFlare Pages 静态内容访问并不需要 Authorizationheader。可以关注:

  • 指向 *.pages.dev或前置 Pages 的自定义域名的 Authorizationheader
  • 在请求中持续出现的非标准 header 名称
  • header 的值更像 token 或 hash,而非标准的 Basic/Bearer 鉴权形态
index=proxy sourcetype=bluecoat OR sourcetype=zscaler
| where like(dest, "%.pages.dev") OR like(dest, "%.workers.dev")
| where isnotnull(http_authorization)
| stats count by src_ip, dest, http_authorization, http_user_agent
| where count < 10

响应码异常

条件访问逻辑会形成一种“二值”响应模式:无效 token 返回 503,有效 token 返回 200。某个主机先收到一串 503,随后突然出现 200,可能意味着 token 使用成功,或发生了分阶段的 payload 拉取。

index=proxy dest="*pages.dev*"
| bin _time span=5m
| stats values(http_status) as status_codes, count by src_ip, dest, _time
| where mvcount(status_codes) > 1 AND match(status_codes, "503") AND match(status_codes, "200")

Content-Type 不匹配

CloudFlare Pages 通常提供的是 Web 内容。如果出现 application/octet-stream或 application/x-msdownload这类二进制 payload 的内容类型,就值得进一步调查:

CommonSecurityLog
| where DestinationHostName endswith ".pages.dev"
| where ResponseContentType in ("application/octet-stream", "application/x-msdownload", "application/x-executable")
| project TimeGenerated, SourceIP, DestinationHostName, RequestURL, ResponseContentType

端点检测

进程网络连接

监控非浏览器进程连接 *.pages.dev或 *.workers.dev的行为,尤其是 LOLBins (certutil、curl、powershell、bitsadmin)、刚落地的可执行文件或低流行度的 exe。

<!-- Sysmon Event ID 3 -->
<RuleGroup groupRelation="and">
&nbsp; <NetworkConnect onmatch="include">
&nbsp; &nbsp; <DestinationHostname condition="end with">.pages.dev</DestinationHostname>
&nbsp; &nbsp; <Image condition="excludes">chrome.exe</Image>
&nbsp; &nbsp; <Image condition="excludes">firefox.exe</Image>
&nbsp; &nbsp; <Image condition="excludes">msedge.exe</Image>
&nbsp; </NetworkConnect>
</RuleGroup>

具有可疑特征的文件下载

关注来自 CloudFlare 基础设施的文件落在异常目录 (temp、appdata、public) 后被立即执行的行为:

DeviceFileEvents
| where RemoteUrl endswith ".pages.dev"
| where FolderPath has_any ("temp", "appdata", "public")
| join kind=inner (DeviceProcessEvents | where Timestamp > ago(5m)) on FileName
| project Timestamp, DeviceName, FileName, FolderPath, RemoteUrl, ProcessCommandLine

DNS 与流量分析

新 Pages 子域名解析

追踪不符合已知业务应用的 Pages 子域名 DNS 查询:

index=dns
| where like(query, "%.pages.dev")
| stats earliest(_time) as first_seen, count by query, src_ip
| where first_seen > relative_time(now(), "-7d")

JA3/JA4 指纹

自定义工具往往具有独特的 TLS 指纹。对连接 CloudFlare Pages 且不匹配已知浏览器或应用的异常指纹进行告警。

Sigma 规则

title: Potential Conditional Access Payload Delivery via Cloudflare Pages
status: experimental
description: Detects non-browser processes accessing Cloudflare Pages with authorization headers
logsource:
&nbsp; &nbsp; category: proxy
detection:
&nbsp; &nbsp; selection:
&nbsp; &nbsp; &nbsp; &nbsp; c-uri|endswith:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.pages.dev'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.workers.dev'
&nbsp; &nbsp; &nbsp; &nbsp; cs-authorization|contains:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'Basic'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'Bearer'
&nbsp; &nbsp; filter:
&nbsp; &nbsp; &nbsp; &nbsp; cs-user-agent|contains:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'Chrome'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'Firefox'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'Edge'
&nbsp; &nbsp; condition: selection and not filter
level: medium
title: Binary Content Downloaded from Static Hosting Platform
id: b2c3d4e5-f6a7-8901-bcde-f12345678901
status: experimental
description: Detects executable content downloaded from static hosting platforms
logsource:
&nbsp; &nbsp; category: proxy
detection:
&nbsp; &nbsp; selection_destination:
&nbsp; &nbsp; &nbsp; &nbsp; c-uri|endswith:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.pages.dev'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.workers.dev'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.netlify.app'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - '.github.io'
&nbsp; &nbsp; selection_content:
&nbsp; &nbsp; &nbsp; &nbsp; rs-content-type:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'application/octet-stream'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; - 'application/x-msdownload'
&nbsp; &nbsp; condition: selection_destination and selection_content
level: high

加固建议

  1. Proxy Policy
  • 对 *.pages.dev的访问要求显式批准,或仅允许已知业务子域名。
  1. DLP Rules
  • 对来自静态托管平台的可执行内容类型进行告警。
  1. Endpoint Controls
  • 通过应用白名单 (allowlisting) 阻止任意二进制执行,不受交付方式影响。
  1. DNS Filtering
  • 如果你内部不使用 CloudFlare Pages,考虑监控该域名模式。

这种技术很难被稳定、可靠地检测:CloudFlare 是覆盖面极大的合法基础设施;HTTPS 会把 Authorizationheader 加密,使得被动观察者看不到;CloudFlare 的边缘 IP 又被数以百万计的网站共享。最佳的检测机会通常出现在能终止 TLS 的代理侧以及端点侧。如果没有 TLS 解密或端点可见性,这些指标会明显更难落地执行。

知道这些以后,也请别去干坏事。


Making CloudFlare Workers Work for Red Teams

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:securitainment Andy Gill《用 CloudFlare Workers 实现有访问控制的 Payload 投递》

评论:0   参与:  0