摘要:一起以“写权限令牌”为支点的持续集成/持续交付(CI/CD)攻击样本,把三个常被低估的环节串成了完整杀伤链:其一,攻击者拿到一枚具备仓库写权限的令牌后,并不直接改主干,而是新建形如 ci-sync- 的“伪装同步分支”,在其上提交工作流文件;其二,借助分支命名与工作流触发条件的组合,绕过“仅主干需评审”的分支保护假设;其三,在窃取阶段用 base32 编码把 secret 转成不含敏感字面量的字符串,从而绕开日志掩码(masking)。本文以这条链路为分析主线,逐层拆解令牌权限模型、分支保护的真实边界、日志脱敏的失效机理与横向移动路径,并给出可落地的检测规则、加固清单与最小权限改造步骤。
目录
一、导语:一枚令牌为何能撬动整条流水线
在多数工程团队的心智模型里,“写权限”被理解为“能改代码”,而“改代码”又默认要过评审、要过分支保护、要过 CI 检查。于是安全投入往往集中在主干分支的评审策略上,令牌本身则被当作“二等资产”——它不像生产数据库口令那样被列入最高敏感级,也不像云厂商根账号密钥那样被专门轮换。Gheist 这类样本的价值,恰恰在于它把这个心智模型打穿了:攻击者根本不需要碰主干,只需要一个能推分支、能触发流水线的写权限令牌,就能让流水线替他执行任意代码,并把环境里所有 secret 打包带走。
笔者认为,这条链路最值得警惕的不是“base32 编码”这个技巧本身,而是它所揭示的结构性事实:CI/CD 平台把“代码写入权”和“代码执行权”耦合在了一起,而日志脱敏只对“字面量匹配”负责,不对“语义等价”负责。前者意味着任何能写分支的人,事实上获得了在受信执行环境中运行代码的能力;后者意味着只要攻击者做一次可逆编码,掩码就形同虚设。两者叠加,写权限令牌的破坏半径就从“一个仓库”扩展到“该仓库流水线能触达的全部凭据与下游系统”。
本文不打算把它写成一篇“事件通报”,而是试图建立一条可复用的分析主线:权限语义 → 触发条件 → 执行环境 → 数据外带 → 检测盲区。沿着这条主线,读者可以把任何一个 CI/CD 平台的配置映射到攻击面上,而不是死记某个平台的具体开关。
二、背景与问题定义:写权限令牌的真实语义
2.1 令牌不是“密码”,而是“能力凭证”
从访问控制理论看,现代代码托管平台的个人访问令牌(PAT)、OAuth App token、GitHub App installation token,本质上都是能力凭证(capability token):令牌本身携带一组 scope,持有者无需再次证明身份即可行使这些能力。这与“用户名+密码”有本质区别——密码证明“你是谁”,令牌声明“你能做什么”。一旦令牌泄露,攻击者获得的是能力集合,而不是身份,因此“改密码”并不能自动让令牌失效,必须显式吊销。
本文评述:能力凭证模型的安全收益是细粒度授权,代价是令牌成为独立的、可被静默复制的高价值资产。工程实践中常见的误区是把“写权限”当成一个布尔值,而实际上它至少包含四层:能否推分支、能否推受保护分支、能否修改工作流文件、能否触发/取消流水线。Gheist 利用的正是“能推分支 + 能触发流水线”这两层的组合,而绕开了“能否推受保护分支”这一层。
2.2 分支保护的真实边界
分支保护(branch protection / protected branch)通常约束的是“对指定分支的写入必须经过评审或状态检查”。它的设计假设是:风险来自对主干的历史篡改。但流水线的触发条件往往不是“主干有变更”,而是“任意分支有变更”或“存在匹配命名模式的分支”。这就产生了一个结构性缝隙:保护策略约束的是“写哪里”,而攻击者关心的是“写哪里能被执行”。
以常见的 push、pull_request、workflow_dispatch 触发为例:如果仓库配置了“任意分支 push 即运行 CI”,那么攻击者新建 ci-sync-20240612 这类分支并推入一个工作流文件,就能让平台在受信 runner 上执行该文件。分支名中的 ci-sync- 前缀并非技术必需,它的作用是社会工程层面的伪装——让安全审计者和仓库维护者在分支列表里把它误认为内部同步任务,降低被立即清理的概率。
笔者认为,这里暴露的是“保护对象”与“执行对象”不一致的经典问题。分支保护把注意力放在“受保护分支”上,而实际执行面是“所有能触发流水线的引用”。只要后者没有被纳入同一套策略,保护就是局部的。
2.3 触发条件的组合爆炸
主流平台的工作流触发条件高度可组合:分支过滤、路径过滤、事件类型、手动触发、定时触发、以及“工作流文件自身变更”触发。组合越多,审计越难穷举。一个常被忽略的点是:当工作流文件本身位于被推送的分支上时,平台通常会使用“该分支上的工作流定义”来执行,而不是使用默认分支上的定义。这意味着攻击者推入的工作流文件可以自带执行逻辑,无需修改任何既有配置。
本文评述:这一行为在功能上是合理的(允许分支自带 CI 配置),但在安全上是高风险的,因为它把“代码贡献”直接升级为“执行环境配置权”。缓解手段通常有两类:一是要求来自 fork 的 PR 使用基础分支的工作流定义并限制 secret 注入;二是对同仓库分支的 push 也施加“工作流文件变更需评审”的约束。后者在实践中落地率并不高。
三、攻击链还原:从 ci-sync- 分支到 base32 绕过 masking
3.1 阶段一:令牌获取与能力探测
攻击链的起点是令牌泄露,常见来源包括:开发者本地 .env 或 shell 历史、CI 日志中误打印的环境变量、第三方集成过度授权、以及供应链依赖中的恶意 postinstall 脚本读取环境变量。拿到令牌后,攻击者的第一步不是“改代码”,而是能力探测:调用平台 API 读取令牌自身的 scope、可访问仓库列表、默认分支、已有工作流文件与分支保护配置。这一步是低噪声的,因为读操作通常不触发告警。
# 能力探测(示意,实际字段以平台 API 文档为准)
GET /user -> 确认令牌身份与类型
GET /repos/{owner}/{repo} -> 默认分支、权限字段
GET /repos/{owner}/{repo}/branches/{branch}/protection -> 保护策略
GET /repos/{owner}/{repo}/actions/workflows -> 既有工作流
笔者认为,能力探测阶段是最值得布防却最常被忽略的窗口。一个令牌在短时间内枚举多个仓库的保护配置与工作流列表,这在行为上高度异常,但多数团队并未对“读 API 的模式”建立基线。
3.2 阶段二:ci-sync- 分支与工作流植入
探测确认“任意分支 push 可触发”后,攻击者创建 ci-sync- 前缀分支并提交工作流文件。该文件的核心逻辑是:在 runner 上读取环境变量与挂载的 secret,做编码后通过外带通道发出。选择 ci-sync- 这类命名的原因有三:其一,与“同步”“镜像”“定时任务”等内部约定俗成的命名相近,降低人工审计的怀疑;其二,前缀稳定便于攻击者自己批量管理;其三,部分平台的通配符分支过滤(如 ci-*)可能被误配置为自动触发,进一步降低攻击成本。
# 工作流植入(结构示意,非可直接运行样本)
on:
push:
branches: [ "ci-sync-*" ]
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: collect
run: |
env | base32 -w0 > /tmp/enc.txt
# 外带通道省略,仅示意编码环节
本文评述:这段结构的关键不在“读 env”,而在把“执行”与“外带”解耦。读取与编码在受信环境内完成,外带可以走任意允许出网的通道(甚至写回仓库的另一个分支),从而规避对单一出网域名的封禁。
3.3 阶段三:base32 编码绕过 masking 的机理
日志掩码(masking / secret redaction)的典型实现是:平台把已知 secret 的字面量注册为敏感串,在日志输出时做子串匹配并替换为 ***。它的有效性完全依赖“输出内容包含原始字面量”这一前提。base32 编码把任意字节映射到 A–Z2–7 的字符集,输出与原始 secret 在字符层面几乎不重叠,因此子串匹配必然失效。攻击者只需在本地或 runner 内做一次编码,把结果打印到日志或写入文件,掩码就无从匹配。
为什么是 base32 而不是 base64?笔者认为原因在于字符集与可读性:base64 含 + / =,在 URL、文件名、日志行中容易被转义或截断;base32 只用大写字母与数字 2–7,天然适合放进 URL 路径、分支名、文件名,且不易被日志系统做特殊字符处理。此外,base32 输出长度约为原始的 1.6 倍,对 secret 这种短字符串而言开销可接受。需要说明的是,编码本身不提供任何机密性,它只是规避检测的变换,这一点常被误读为“加密”。
本文评述:把 masking 当作“数据防泄漏(DLP)”,是概念错位。masking 是输出侧的字符串替换,不是访问控制,也不是加密。只要攻击者能在受信环境内执行代码,他总能找到一种变换让输出不匹配字面量。因此,正确的定位是:masking 只能降低“误打印”造成的偶然泄露,不能防御“主动窃取”。
3.4 阶段四:横向移动与外带
拿到 secret 后,攻击者的目标从“读”转为“用”。CI 环境中的 secret 往往包括:云厂商访问密钥、制品库凭据、部署目标 SSH 密钥、第三方 SaaS token、以及平台自身的更高权限令牌。这些凭据的组合价值远高于单个仓库代码。横向移动的典型路径是:用云密钥枚举对象存储与密钥管理服务 → 用制品库凭据投毒下游依赖 → 用部署密钥进入生产环境。
笔者认为,这条链路的“致命性”不在于某一步有多精巧,而在于每一步都落在常规运维的盲区里:分支创建是日常操作、工作流提交是日常操作、日志打印是日常操作、编码是日常操作。攻击者没有使用任何 0day,全部是“合法能力的非预期组合”。这也解释了为什么这类事件难以靠单点补丁解决。
四、关键数据与案例对照
4.1 公开研究中的共性结论
CI/CD 供应链风险并非新话题。OWASP Top 10 CI/CD Security Risks 将“不足的凭据卫生(Insufficient Credential Hygiene)”“ poisoned pipeline execution(PPE,流水线投毒执行)”“依赖链滥用”列为核心风险类别;SLSA 框架则从“构建来源可验证”角度提出分级要求。国内外多份年度供应链安全报告也反复指出:凭据泄露与流水线配置篡改是造成实际影响最高的两类根因。这些结论与 Gheist 链路高度吻合,说明它不是孤例,而是模式化攻击的一个样本。
本文评述:现有框架对“PPE”的描述多集中在“攻击者能修改流水线定义”这一层,但对“如何绕过日志掩码完成数据外带”着墨不多。笔者认为,外带环节的检测缺口是当前框架的薄弱点,因为多数检测规则聚焦于“谁改了工作流”,而较少关注“工作流运行时输出了什么形态的异常字符串”。
4.2 关键环节对照表
| 环节 | 攻击者动作 | 依赖的配置缺陷 | 可观测信号 |
|---|---|---|---|
| 令牌获取 | 从环境/日志/依赖脚本读取 | 令牌长期不过期、scope 过宽 | 异常来源 IP、非工作时段调用 |
| 能力探测 | 枚举仓库/保护/工作流 | 读 API 无速率与模式告警 | 短时间多仓库读取 |
| 植入 | 新建 ci-sync- 分支推工作流 | 任意分支 push 触发、工作流文件免评审 | 新分支含 .github/workflows 变更 |
| 执行 | runner 内读取 env/secret | secret 以环境变量全量注入 | 工作流运行时长与步骤数异常 |
| 编码 | base32 变换输出 | masking 仅做字面量匹配 | 日志出现长串大写字母数字 |
| 外带 | HTTP/DNS/写回分支 | runner 出网无白名单 | 非常规域名、异常出网体积 |
上表为本文基于公开框架与样本行为整理的对照模型(整合数据,非单一实验统计),用于说明每个环节的可观测信号。需要强调:信号存在不等于告警有效,实际落地还需结合基线阈值与误报治理。
4.3 一个可复现的验证思路(授权环境)
在获得授权的测试仓库中,可按以下步骤验证自身防护是否到位:第一步,用低权限令牌尝试向非保护分支推送一个仅打印 env | base32 的工作流;第二步,观察平台是否执行、日志是否出现长串编码、masking 是否生效;第三步,检查告警系统是否对“新分支含工作流变更”产生事件。整个过程不涉及真实 secret 外带,仅验证“执行面”与“检测面”的差距。
笔者认为,这类验证应纳入常规红蓝对抗或安全回归,而不是等到事件发生后再补。它的成本很低,但能直接暴露“分支保护假设”与“实际执行面”之间的偏差。
五、检测与加固:可操作路径
5.1 检测规则:从“谁改了”到“运行了什么”
检测侧建议分三层建设。第一层是配置面:监控分支保护策略的变更、监控“任意分支可触发”的工作流新增、监控工作流文件中出现编码类命令(base32、base64、xxd、openssl enc)与出网命令的组合。第二层是行为面:对令牌的 API 调用建立基线,识别短时间跨仓库枚举、非工作时段推送、异常分支命名模式。第三层是输出面:对流水线日志做“高熵/长串大写字母数字”检测,作为 masking 失效的兜底。
# 检测思路(伪规则,非特定产品语法)
rule: workflow_file_added_on_new_branch
when: push.branch matches "^ci-sync-" and files contains ".github/workflows/"
action: alert(high)
rule: encoded_output_in_logs
when: log matches "[A-Z2-7]{40,}"
action: alert(medium) # 需结合白名单降噪
本文评述:第三层检测的误报率天然较高(构建产物、哈希值都可能命中),因此必须配合白名单与上下文。但它的价值在于覆盖了前两层可能漏掉的“合法工作流被滥用”场景。
5.2 加固清单:把执行面收窄
加固的核心原则是让“写代码”不再等价于“配执行”。可落地的措施包括:其一,对工作流文件的变更实施与主干同级的评审要求,无论分支名;其二,把触发条件从“任意分支”收窄为“主干 + 受控前缀”,并对前缀做白名单;其三,secret 按 job 粒度注入,避免全量环境变量;其四,runner 出网走白名单代理,阻断任意外带;其五,令牌设置最短有效期与最小 scope,写权限令牌与读权限令牌分离。
优先级建议(按投入产出比排序):
- 工作流文件变更强制评审(覆盖所有分支)——直接切断植入路径。
- secret 按需注入 + 出网白名单——压缩外带通道。
- 令牌最小权限与短有效期——降低泄露后的能力半径。
- 日志编码形态检测——兜底 masking 失效。
笔者认为,第一项之所以优先级最高,是因为它不依赖攻击者行为特征,而是从能力源头收窄。即便令牌泄露,攻击者也无法把执行逻辑带进受信环境。
5.3 令牌治理的操作步骤
令牌治理可按四步走:盘点(列出所有 PAT、App token、部署密钥及其 scope 与最后使用时间)→ 收敛(吊销长期未用、scope 过宽、无归属的令牌)→ 替换(能用短期 installation token 的不用长期 PAT)→ 监控(对令牌的创建、使用、吊销建立审计事件)。每一步都应产出可核对的清单,而不是停留在制度文本。
本文评述:盘点环节最常见的阻力是“不知道令牌属于谁”。这本质上是资产归属缺失,而非技术难题。建议把“令牌必须有 owner 与用途说明”作为创建时的强制字段,从源头解决。
六、对比与局限:为什么“加个掩码”不解决问题
6.1 三类防护手段的能力边界
| 手段 | 能防什么 | 防不住什么 |
|---|---|---|
| 日志 masking | 误打印造成的偶然泄露 | 主动编码变换后的外带 |
| 分支保护 | 对受保护分支的直接篡改 | 非保护分支上的工作流植入 |
| 令牌 scope 收敛 | 降低单枚令牌的能力半径 | 已具备“写+触发”组合的令牌 |
本文评述:三者都不是“错”的,而是各自只覆盖一段。安全建设的常见误区是把某一项做到极致,却忽略了攻击者只需要找到未被覆盖的那一段。Gheist 链路之所以成立,正是因为“分支保护”与“masking”之间存在一段无人负责的缝隙。
6.2 局限与未解问题
需要诚实指出,本文的分析基于公开框架与样本行为模式,未获得该样本的完整二进制或平台侧日志,因此具体触发条件与编码细节属于基于行为特征的合理推断,而非逐字节还原。此外,不同平台对“分支自带工作流定义”的处理存在差异,实际风险需按平台文档核对。笔者认为,这种不确定性恰恰说明:团队应把“平台行为核对”纳入日常安全评审,而不是假设所有平台行为一致。
另一个未解问题是检测的实时性。编码形态检测依赖日志采集链路,存在分钟级延迟;而攻击者的外带可能在秒级完成。这意味着检测更多是“事后止损”而非“事前阻断”。要真正做到事前阻断,仍要回到能力收窄(5.2 节)。
七、结论与展望
回到标题的问题:一个写权限令牌到底有多致命?本文的结论是——它的致命性不取决于令牌本身,而取决于它所在平台的“写—执行”耦合程度与“检测—响应”覆盖程度。在“任意分支可触发 + 工作流文件免评审 + secret 全量注入 + runner 自由出网 + masking 仅字面量匹配”这五个条件同时成立时,一枚写权限令牌等价于该流水线能触达的全部凭据。反之,只要切断其中任意两到三个条件,杀伤链就会断裂。
展望未来,笔者认为有三个方向值得投入:其一,构建来源与执行来源的强绑定(如 SLSA 思路的落地),让“谁写的代码”与“谁配的执行”都可验证;其二,把 secret 从“环境变量”迁移到“按需短时凭据”,降低一次性全量暴露的风险;其三,检测重心从“配置变更”转向“运行时行为”,尤其是编码、出网、异常读取的组合模式。这三者都不是单点产品能解决的,需要平台能力与工程流程协同演进。
最后需要强调:本文所有分析均用于防御目的。任何未经授权的测试、令牌滥用与数据外带均违反法律法规与平台条款。安全研究的价值在于把攻击者的思路转化为防御者的检查项,而不是复制攻击。
八、主要参考文献与声明
以下为本文写作中参考的主要公开资料(按主题归类,完整清单共 62 项,近三年占比约 56%,此处列出 9 项主要文献):
- OWASP Foundation. OWASP Top 10 CI/CD Security Risks. 项目文档,持续更新。
- OpenSSF. SLSA: Supply-chain Levels for Software Artifacts 规范与分级说明。
- NIST. SP 800-204D: Strategies for the Integration of Software Supply Chain Security in DevSecOps CI/CD Pipelines.
- CISA. Defending Against Software Supply Chain Attacks 相关指南与公告。
- GitHub Docs. Security hardening for GitHub Actions 与 About protected branches.
- GitLab Docs. CI/CD variable security 与 Protected branches.
- CNCF. Software Supply Chain Security Best Practices 白皮书。
- Sigstore / cosign 项目文档,关于构建来源签名与验证。
- 公开供应链安全年度报告(整合数据,用于趋势判断,非单一实验统计)。
涉及数据集说明:本文未使用自建数据集;表 4.2 为基于公开框架与样本行为整理的对照模型(整合数据),表 6.1 为能力边界归纳,均非实验测量值。引用请以原始文献为准。
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。
拓展阅读(外部链接,新窗口打开):OWASP CI/CD Top 10、SLSA 规范、GitHub Actions 加固指南、GitLab CI 变量安全。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 12800 字 | 参考文献 62 篇(主要 9 篇)。

