Send()-ing Myself Belated Christmas Gifts – GitHub.com’s Environment Variables & GHES Shell

Earlier this year, in mid-January, you might have come across this security announcement by GitHub.
今年早些时候,在1月中旬,你可能会看到GitHub的这个安全公告。

In this article, I will unveil the shocking story of how I discovered CVE-2024-0200, a deceptively simple, one-liner vulnerability which I initially assessed to likely be of low impact, and how I turned it into one of the most impactful bugs in GitHub’s bug bounty history.
在这篇文章中,我将揭开我如何发现CVE-2024-0200的令人震惊的故事,这是一个看似简单的单行漏洞,我最初评估它可能影响不大,以及我如何将其变成GitHub错误赏金历史上最具影响力的错误之一。

Spoiler: The vulnerability enabled disclosure of all environment variables of a production container on GitHub.com, including numerous access keys and secrets. Additionally, this vulnerability can be further escalated to achieve remote code execution (RCE) on GitHub Enterprise Servers (GHES), but not on GitHub.com. More on this later.
剧透:该漏洞允许泄露 GitHub.com 上生产容器的所有环境变量,包括许多访问密钥和秘密。此外,此漏洞可以进一步升级,以在GitHub企业服务器(GHES)上实现远程代码执行(RCE),而不是在 GitHub.com 上。稍后再详细介绍。

Backstory 背景故事

Back in early December 2023, I was performing some research on GHES. On the day before I went on vacation, I located a potential (but likely minor) bug. Fast-forward to the day after Christmas, I finally found some time to triage and analyse this potential vulnerability. At that point, I still had zero expectations of the potential bug to be this impactful… until an accident happened.
早在2023年12月初,我正在对GHES进行一些研究。在我去度假的前一天,我发现了一个潜在的(但可能是小的)bug。快进到圣诞节后的第二天,我终于找到了一些时间来分类和分析这个潜在的漏洞。在这一点上,我仍然没有期望潜在的错误是如此有影响力……直到事故发生。

A Quick Primer on Ruby Reflections
Ruby Reflections的快速入门

Before I spill the tea, allow me to begin with a brief introduction to Ruby.
在我把茶洒出来之前,请允许我开始简要介绍一下Ruby。

Similar to JavaScript, almost everything (e.g. booleans, strings, integers) is an Object in Ruby. The Object includes the Kernel module as a mixin, rendering methods in the Kernel module accessible by every Ruby object. Notably, it is possible to do reflection (i.e. indirect method invocation) by using Kernel#send() as such:
与JavaScript类似,几乎所有的东西(例如布尔值、字符串、整数)在Ruby中都是#0 #。 Object 包含 Kernel 模块作为mixin,呈现每个Ruby对象都可以访问的 Kernel 模块中的方法。值得注意的是,可以通过使用 Kernel#send() 进行反射(即间接方法调用):

Class HelloWorld
  def print(*args)
    puts("Hello " + args.join(' '))
  end
end

obj = HelloWorld.new()
obj.print('world') # => 'Hello World'
obj.send('print', 'world') # => 'Hello World'

As shown above, it is possible to dynamically invoke a method using Kernel#send() to perform reflection on any object. Naturally, this makes it an obvious code sink to search for, since having ability to invoke arbitrary methods on an object can be disastrous. For example, unsafe reflections with 2 controllable arguments can easily lead to arbitrary code execution:
如上所示,可以使用 Kernel#send() 动态调用方法来对任何对象执行反射。自然地,这使它成为一个明显的要搜索的代码接收器,因为在对象上调用任意方法的能力可能是灾难性的。例如,带有2个可控参数的不安全反射很容易导致任意代码执行:

user_input1 = 'eval'
user_input2 = 'arbitrary Ruby code here'

obj.send(user_input1, user_input2)
# is equivalent to:
obj.send('eval', 'arbitrary Ruby code here')
# which is equivalent to:
Kernel.eval('arbitrary Ruby code here')
# which has the same effect as:
eval('arbitrary Ruby code here')
# note: because everything is an object, including the current context

If you have more than 2 controllable arguments, then it is also trivial to achieve arbitrary code execution by calling send() repeatedly:
如果你有2个以上的可控参数,那么通过重复调用 send() 来实现任意代码执行也是微不足道的:

obj.send('send', 'send', 'send', 'send', 'eval', '1+1')
# will call: 
obj.send('send', 'send', 'send', 'eval', '1+1')
# ...
obj.send('eval', '1+1')
# to finally call:
eval('1+1')

You can read more about unsafe reflections in Phrack Issue 0x45 by @joernchen or this Ruby security discussion published on Seebug (in Chinese).
您可以在Phrack Issue 0x 45 by @joernchen或Seebug上发布的Ruby安全讨论中阅读更多关于不安全反射的内容。

Interestingly enough, I did not come across any discussions on unsafe reflections with only 1 controllable argument in Kernel#send() as shown below:
有趣的是,我没有遇到任何关于不安全反射的讨论,在 Kernel#send() 中只有一个可控参数,如下所示:

user_input = 'method_name_here'
obj.send(user_input)

At first glance, it seems rather difficult to escalate impact at all in this scenario. From the list of default methods inherited from Object, I identified the following useful methods:
乍一看,在这种情况下,似乎很难升级影响。从从 Object 继承的默认方法列表中,我确定了以下有用的方法:

# Disclosing filepaths:
obj.send('__dir__') # leak resolved absolute path to directory containing current file
obj.send('caller')  # return execution call stack, and may leak filepaths

# Disclosing class name
obj.send('class')

# Disclosing method names
obj.send('__callee__')
obj.send('__method__')
obj.send('matching_methods')
obj.send('methods') # Object#methods() returns list of public and protected methods
obj.send('private_methods')
obj.send('protected_methods')
obj.send('public_methods')
obj.send('singleton_methods')

# Disclosing variable names
obj.send('instance_variables')
obj.send('global_variables')
obj.send('local_variables')

# Stringify variable
obj.send('inspect') # calls to_s recursively
obj.send('to_s')    # string representation of the object

# Read from standard input
obj.send('gets')
obj.send('readline')
obj.send('readlines')

# Terminates process (please exercise caution)
obj.send('abort')
obj.send('fail')
obj.send('exit')
obj.send('exit!')

These methods may come in handy when attempting to gather more information on the target, especially when performing blind, unsafe reflections.
当试图收集更多关于目标的信息时,这些方法可能会派上用场,特别是在执行盲反射时。

However, in the case of GitHub, this won’t be necessary since we can audit the source code of GHES, which is largely identical to the one deployed on GitHub.com. Now, we are ready to move on to discuss the vulnerability.
然而,在GitHub的情况下,这将是不必要的,因为我们可以审计GHES的源代码,这在很大程度上是相同的部署在 GitHub.com 上。现在,我们准备继续讨论漏洞。

Discovering the Vulnerability
发现漏洞

Note: The source code presented below were extracted from GitHub Enterprise Server (GHES) 3.11.0 to pinpoint the root cause of the vulnerability.
注意:下面的源代码是从GitHub Enterprise Server(GHES)3.11.0中提取的,以查明漏洞的根本原因。

Doing a quick search on the codebase, I found an unvalidated Kernel#send() call in Organizations::Settings::RepositoryItemsComponent found in app/components/organizations/settings/repository_items_component.rb:
在代码库上做了一个快速搜索,我在 Organizations::Settings::RepositoryItemsComponent 中发现了一个未经验证的 Kernel#send() 调用,在 app/components/organizations/settings/repository_items_component.rb 中发现:

...
class Organizations::Settings::RepositoryItemsComponent < ApplicationComponent
  def initialize(organization:, repositories:, selected_repositories:, current_page:, total_count:, data_url:, aria_id_prefix:, repository_identifier_key: :global_relay_id, form_id: nil)
    @organization = organization
    @repositories = repositories
    @selected_repositories = selected_repositories
    @show_next_page = current_page * Orgs::RepositoryItemsHelper::PER_PAGE < total_count
    @data_url = data_url
    @current_page = current_page
    @aria_id_prefix = aria_id_prefix
    @repository_identifier_key = repository_identifier_key # [2]
    @form_id = form_id
  end
  ...
  def identifier_for(repository)
    repository.send(@repository_identifier_key) # [1]
  end
  ...
end

At [1], repository.send(@repository_identifier_key) is invoked in the identifier_for() method without any prior input validation on @repository_identifier_key (set at [2]). This allows all methods accessible by the object (including private or protected methods, and any other methods inherited from ancestor classes) to be invoked.
在[1]中, repository.send(@repository_identifier_key) 在 identifier_for() 方法中被调用,而没有对 @repository_identifier_key 进行任何先前的输入验证(设置在[2]中)。这允许调用对象可访问的所有方法(包括私有或受保护的方法,以及从祖先类继承的任何其他方法)。

The identifier_for() method of the Organizations::Settings::RepositoryItemsComponent class is used in app/components/organizations/settings/repository_items_component.html.erb (the template file to be rendered and returned within the HTTP response body) at [3]:
Organizations::Settings::RepositoryItemsComponent 类的 identifier_for() 方法在 app/components/organizations/settings/repository_items_component.html.erb (在HTTP响应体中呈现和返回的模板文件)中使用[3]:

<%# erblint:counter ButtonComponentMigrationCounter 1 %>
<% @repositories.each do |repository| %>
  <li <% unless first_page? %> hidden <% end %> class="css-truncate d-flex flex-items-center width-full">
    <input
      <%= "form=#{@form_id}" if @form_id.present? %>
      type="checkbox" name="repository_ids[]"
      value="<%= identifier_for(repository) %>" # [3]
      id="<%= @aria_id_prefix %>-<%= repository.id %>"
...

Backtracing further, it can be seen that Organizations::Settings::RepositoryItemsComponent objects are initialised in app/controllers/orgs/actions_settings/repository_items_controller.rb:
进一步回溯,可以看到 Organizations::Settings::RepositoryItemsComponent 对象在 app/controllers/orgs/actions_settings/repository_items_controller.rb 中初始化:

class Orgs::ActionsSettings::RepositoryItemsController < Orgs::Controller
  ...

  def index
    ...
    respond_to do |format|
      format.html do
        render(Organizations::Settings::RepositoryItemsComponent.new(
          organization: current_organization,
          repositories: additional_repositories(selected_repository_ids),
          selected_repositories: [],
          current_page: page,
          total_count: current_organization.repositories.size,
          data_url: data_url,
          aria_id_prefix: aria_id_prefix,
          repository_identifier_key: repository_identifier_key, # [4]
          form_id: form_id
        ), layout: false)
      end
    end
  end

  ...

  def rid_key
    params[:rid_key] # [6]
  end

  ...

  def repository_identifier_key
    return :global_relay_id unless rid_key.present? # [5]
    rid_key
  end

  ...
end

At [4], the result of the repository_identifier_key() method is passed as the repository_identifier_key keyword argument when initialising the Organizations::Settings::RepositoryItemsComponent object. At [5], in repository_identifier_key(), observe that :global_relay_id is returned only when the return value of rid_key() is absent. Otherwise, the repository_identifier_key() method simply passes on the return value from rid_key() – params[:rid_key] (at [6]).
在[4]中,当初始化 Organizations::Settings::RepositoryItemsComponent 对象时, repository_identifier_key() 方法的结果作为 repository_identifier_key 关键字参数传递。在[5]中,在 repository_identifier_key() 中,观察到只有当 rid_key() 的返回值不存在时才返回 :global_relay_id 。否则, repository_identifier_key() 方法只是传递来自 rid_key() – params[:rid_key] 的返回值(在[6])。

Putting it all together, the unsafe reflection repository.send(@repository_identifier_key) allows for a “zero-argument arbitrary method invocation” on a Repository object.
把所有这些放在一起,不安全反射 repository.send(@repository_identifier_key) 允许在 Repository 对象上进行“零参数任意方法调用”。

In Search of Impact 寻找影响力

This is exactly the same scenario I discussed earlier. Unfortunately, none of the options I shared earlier are applicable in this case – the information is likely available to us already, or they do not do anything useful for us at this point. So, how can we escalate the impact further?
这与我之前讨论的场景完全相同。不幸的是,我之前分享的选项都不适用于这种情况-信息可能已经提供给我们,或者他们在这一点上对我们没有任何用处。那么,我们如何才能进一步扩大影响?

It is crucial to recognise that we are not limited to methods inherited from Object – we can expand the search of candidate methods by looking at methods accessible by a Repository object.
重要的是要认识到,我们并不局限于从 Object 继承的方法-我们可以通过查看可由 Repository 对象访问的方法来扩展候选方法的搜索。

Next, let’s refer back to the assumption of having a “zero-argument arbitrary method invocation”. What does that even mean? Can we only invoke methods accepting no arguments at all?
接下来,让我们回到“零参数任意方法调用”的假设。那是什么意思我们可以只调用不接受任何参数的方法吗?

The answer is: No. Surprise!
答案是:不,惊喜!

Actually, this is a common misassumption with a pretty straightforward counter-example to disprove it (as shown below):
实际上,这是一个常见的错误假设,有一个非常简单的反例来证明它(如下所示):

class Test
  # zero arguments required
  def zero_arg()
  end

  # 1 positional argument required
  def one_pos_arg(arg1)
  end

  # 2 positional arguments required, but second argument has default value
  def one_pos_arg_one_default_pos_arg(arg1, arg2 = 'default')
  end

  # 2 positionals argument required, but both arguments have default values
  def two_default_pos_args(arg1 = 'default', arg2 = 'default')
  end

  # 1 keyword argument (similar to positional argument) required (no default value)
  def one_keyword_arg(keyword_arg1:)
  end

  # 1 keyword argument (has default value) required
  def one_default_keyword_arg(keyword_arg1: 'default')
  end

  # 1 positional (no default value) & 1 keyword argument (has default value) required
  def one_pos_arg_one_default_keyword_arg(arg1, keyword_arg2: 'default') 
  end
end

obj = Test.new()
obj.send('zero_arg')                            # => OK
obj.send('one_pos_arg')                         # => in `b': wrong number of arguments (given 1, expected 0) (ArgumentError)
obj.send('one_pos_arg_one_default_pos_arg')     # => in `c': wrong number of arguments (given 1, expected 1..2) (ArgumentError)
obj.send('two_default_pos_args')                # => OK
obj.send('one_keyword_arg')                     # => in `e`: missing keyword: :keyword_arg1 (ArgumentError)
obj.send('one_default_keyword_arg')             # => OK
obj.send('one_pos_arg_one_default_keyword_arg') # => OK

Clearly, we are able to invoke methods requiring arguments just fine – so long as they have default values assigned to them!
显然,我们能够很好地调用需要参数的方法——只要它们被赋以默认值!

With these two tricks in our bag, we are now ready to start searching for candidate methods… but how? We can simply grep until we find something useful, but this will be a tedious process. The main Docker image containing the Ruby on Rails application source code contains more than a whopping 100k files (~1.5 GB), so we clearly need a better strategy.
有了这两个技巧,我们现在可以开始寻找候选方法了.但是怎么做呢?我们可以简单地 grep ,直到我们找到有用的东西,但这将是一个繁琐的过程。包含Ruby on Rails应用程序源代码的主Docker镜像包含超过10万个文件(约1.5 GB),因此我们显然需要更好的策略。

A simple solution to this complex task is just to drop into a Rails console in a test GHES setup, and use reflection to aid us in our quest:
这个复杂任务的一个简单解决方案是在测试GHES设置中进入Rails控制台,并使用反射来帮助我们完成任务:

repo = Repository.find(1) # get first repo
methods = [ # get names of all methods accessible by Repository object
  repo.public_methods(),
  repo.private_methods(),
  repo.protected_methods(),
].flatten()

methods.length() # => 5542

Yes, you read that correctly. I was quite shocked when I saw the output too.
是的,你没看错。当我看到输出时,我也很震惊。

Why on earth does the Repository object even have 5542 methods? Well, to be fair, most of it came from autogenerated code by Ruby on Rails, which leverages Ruby metaprogramming to define getters/setter methods on objects.
为什么0#对象有5542个方法?好吧,公平地说,大部分来自Ruby on Rails自动生成的代码,它利用Ruby元编程来定义对象的getter/setter方法。

Let’s further reduce the search space by finding methods matching the criteria (i.e. no required positional or keyword arguments that do not have default values). This is because we need to prevent Ruby from throwing ArgumentError due to obvious mismatch of the number of required arguments. Going back to the previous example on the Test class, let’s examine the arity of the method:
让我们通过查找匹配条件的方法来进一步减少搜索空间(即不需要没有默认值的位置或关键字参数)。这是因为我们需要防止Ruby由于所需参数的数量明显不匹配而抛出 ArgumentError 。回到前面关于 Test 类的例子,让我们检查一下方法的arity:

class Test
  # zero arguments required
  def zero_arg()
  end

  # 1 positional argument required
  def one_pos_arg(arg1)
  end

  # 2 positional arguments required, but second argument has default value
  def one_pos_arg_one_default_pos_arg(arg1, arg2 = 'default')
  end

  # 2 positionals argument required, but both arguments have default values
  def two_default_pos_args(arg1 = 'default', arg2 = 'default')
  end

  # 1 keyword argument (similar to positional argument) required (no default value)
  def one_keyword_arg(keyword_arg1:)
  end

  # 1 keyword argument (has default value) required
  def one_default_keyword_arg(keyword_arg1: 'default')
  end

  # 1 positional (no default value) & 1 keyword argument (has default value) required
  def one_pos_arg_one_default_keyword_arg(arg1, keyword_arg2: 'default') 
  end
end

obj = Test.new()
obj.method('zero_arg').arity()                            # => 0
obj.method('one_pos_arg').arity()                         # => 1
obj.method('one_pos_arg_one_default_pos_arg').arity()     # => -2
obj.method('two_default_pos_args').arity()                # => -1
obj.method('one_keyword_arg').arity()                     # => 1
obj.method('one_default_keyword_arg').arity()             # => -1
obj.method('one_pos_arg_one_default_keyword_arg').arity() # => -2

It appears that only methods with arity of 0 or -1 can be used by us in this case.
在这种情况下,我们似乎只能使用arity为 0 或 -1 的方法。

Now, we can filter the list of candidate methods further:
现在,我们可以进一步过滤候选方法列表:

repo = Repository.find(1)  # get first repo
repo_methods = [           # get names of all methods accessible by Repository object
  repo.public_methods(),
  repo.private_methods(),
  repo.protected_methods(),
].flatten()

repo_methods.length()      # => 5542
candidate_methods = repo_methods.select() do |method_name|
  [0, -1].include?(repo.method(method_name).arity())
end
candidate_methods.length() # => 3595

I guess that is slightly better…? Metaprogramming can be a curse sometimes. 😅
我想这样稍微好一点…?元编程有时可能是一个诅咒。😅

While I could further reduce the search space, I didn’t want to risk having missing out on any potentially useful functions. It is probably a good idea to scan through the output to get a better sensing of what methods are available first before further processing.
虽然我可以进一步减少搜索空间,但我不想冒险错过任何潜在的有用功能。在进一步处理之前,扫描输出以更好地了解哪些方法是可用的可能是一个好主意。

Let’s dump the location of where the methods are defined:
让我们转储方法定义的位置:

candidate_methods.map!() do |method_name|
  method = repo.method(method_name)
  [
    method_name,
    method.arity(),
    method.source_location()
  ]
end
puts(candidate_methods.sort())

The output is a long list of 3595 methods and their location:
输出是一个包含3595个方法及其位置的长列表:

[
  [:!, [0, nil]],
  [:Nn_, [-1, ["/github/vendor/gems/3.2.2/ruby/3.2.0/gems/fast_gettext-2.2.0/lib/fast_gettext/translation.rb", 65]]],
  [:_, [-1, ["/github/vendor/gems/3.2.2/ruby/3.2.0/gems/gettext_i18n_rails-1.8.1/lib/gettext_i18n_rails/html_safe_translations.rb", 10]]],
  [:__callbacks, [0, ["/github/vendor/gems/3.2.2/ruby/3.2.0/gems/activesupport-7.1.0.alpha.bb4dbd14f8/lib/active_support/callbacks.rb", 70]]],
  ...
  [:xcode_clone_url, [-1, ["/github/app/helpers/url_helper.rb", 218]]],
  [:xcode_project?, [0, ["/github/packages/repositories/app/models/repository/git_dependency.rb", 323]]],
  [:xcode_urls_enabled?, [0, ["/github/app/helpers/url_helper.rb", 213]]],
  [:yield_self, [0, ["<internal:kernel>", 144]]]
]

Triaging Candidate Methods
分类候选方法

I started triaging the list of potentially useful methods, but as I was testing locally, I realised my test GHES installation wasn’t working correctly somehow and had to be re-installed. At this point, I noticed most of the methods would likely only affect my own organisation repositories, or allow me to leak some information like file paths on the server, which may come in handy in the future.
我开始筛选可能有用的方法列表,但当我在本地测试时,我意识到我的测试GHES安装不正确,必须重新安装。在这一点上,我注意到大多数方法可能只会影响我自己的组织存储库,或者允许我泄露一些信息,如服务器上的文件路径,这可能在未来派上用场。

I didn’t really want to waste precious time doing nothing while waiting for the re-installation to complete, so I decided to test it on the production GitHub.com server in my own test organisation given the current potential impact achievable.
我真的不想浪费宝贵的时间什么都不做,而等待重新安装完成,所以我决定在我自己的测试组织中的生产 GitHub.com 服务器上测试它,因为目前可能会产生影响。

Nothing could possibly go wrong, right…? 🙈
不会出什么差错的,对吧…?🙈

Wrong. I was completely off the mark. The following response came back shortly after I began testing on the production GitHub.com server, leaving me completely speechless and stunned in disbelief:
错了我完全错了。在我开始在生产 GitHub.com 服务器上进行测试后不久,就返回了以下响应,这让我完全说不出话来,并且难以置信地惊呆了:
Send()-ing Myself Belated Christmas Gifts - GitHub.com's Environment Variables & GHES Shell

That’s ~2MB worth of environment variables belonging to GitHub.com containing a massive list of access keys and secrets within the response body. How did these secrets end up here?!
这是属于 GitHub.com 的大约2MB的环境变量,其中包含响应主体中的大量访问密钥和秘密列表。这些秘密怎么会在这里!

Getting the Environment Variables
获取环境变量

Let’s examine the Repository::GitDependency module (at packages/repositories/app/models/repository/git_dependency.rb) included by the Repository containing the dangerous method nw_fsck():
让我们检查包含危险方法 nw_fsck() 的 Repository 所包含的 Repository::GitDependency 模块(在 packages/repositories/app/models/repository/git_dependency.rb ):

module Repository::GitDependency
  ...
  def nw_fsck(trust_synced: false)
    rpc.nw_fsck(trust_synced: trust_synced)
  end
  ...
end

Note: In Ruby, the last evaluated line of any method/block is the implicit return value.
注意:在Ruby中,任何方法/块的最后一行都是隐式返回值。

This nw_fsck() method is extremely inconspicuous, but holds a wealth of information. To understand why, let’s examine the GitRPC backend implementation in vendor/gitrpc/lib/gitrpc/backend/nw.rb:
这个 nw_fsck() 方法非常不显眼,但包含了丰富的信息。为了理解为什么,让我们看看 vendor/gitrpc/lib/gitrpc/backend/nw.rb 中的GitRPC后端实现:

module GitRPC
  class Backend
    ...
    rpc_writer :nw_fsck, output_varies: true
    def nw_fsck(trust_synced: false)
      argv = []
      argv << "--connectivity-only"
      argv << "--trust-synced" if trust_synced
      spawn_git("nw-fsck", argv) # [7]
    end
    ...
  end
end

At [7], the return value of the nw_fsck() method is the git process created using the spawn_git() method. The spawn_git() method eventually calls and returns GitRPC::Native#spawn():
在[7]中, nw_fsck() 方法的返回值是使用 spawn_git() 方法创建的 git 进程。 spawn_git() 方法最终调用并返回 GitRPC::Native#spawn() :

...
module GitRPC
  ...
  class Native
    ...
    def spawn(argv, input = nil, env = {}, options = {})
      ...
      {
        # Report unhandled signals as failure
        :ok        => !!process.status.success?,
        # Report the exit status as the signal number if we have it
        :status    => process.status.exitstatus || process.status.termsig,
        :signaled  => process.status.signaled?,
        :pid       => process.status.pid,
        :out       => process.out,
        :err       => process.err,
        :argv      => argv,
        :env       => env, # [8]
        :path      => @path,
        :options   => options,
        :truncated => truncated,
      }
    end
    ...
  end
end

Observe that the value returned by GitRPC::Native.spawn() is a Hash object containing the environment variables passed to the git process at [8]. Digging deeper, the list of environment variables passed to the git process created can be found in vendor/gitrpc/lib/gitrpc/backend.rb:
注意, GitRPC::Native.spawn() 返回的值是一个 Hash 对象,包含在[8]传递给 git 进程的环境变量。深入挖掘,传递给 git 进程的环境变量列表可以在 vendor/gitrpc/lib/gitrpc/backend.rb 中找到:

...
module GitRPC
  ...
  class Backend
    ...
    def self.environment
      @environment ||= ENV.to_h.freeze # [10]
    end
    ...
    def native
      @native ||= GitRPC::Native.new(path, native_env, native_options)
    end
    ...
    def native_env
      env = GitRPC::Backend.environment.dup # [9]
      env.merge!(options[:env] || {})
      env.merge!(GitRPC.extra_native_env || {})
      env["GITHUB_TELEMETRY_LOGS_NOOP"] = "true"
      env["GIT_DIR"] = path
      env["GIT_LITERAL_PATHSPECS"] = "1"
      env["GIT_SOCKSTAT_VAR_via"] = "gitrpc"
      if options[:info]
        git_sockstat_var_options.each do |(prefix, sym)|
          env["GIT_SOCKSTAT_VAR_#{sym}"] = "#{prefix}#{options[:info][sym]}" if options[:info][sym]
        end
      end
      if alternates = alternate_object_paths
        env["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = alternates.join(":")
      end
      env
    end
    ...
  end
end

At [9], GitRPC::Backend#native_env() duplicates the environment variable Hash object returned by GitRPC::Backend::environment() at [10], which is basically a copy of all environment variables passed to Rails.
在[9]中, GitRPC::Backend#native_env() 复制了由 GitRPC::Backend::environment() 在[10]中返回的环境变量 Hash 对象,这基本上是传递给Rails的所有环境变量的副本。

Since GitRPC::Native#spawn() returns the list of environment variables in a Hash, and Repository::GitDependency#nw_fsck() kindly returns the Hash to us, we are able to disclose all the environment variables passed to Rails!
由于 GitRPC::Native#spawn() 在 Hash 中返回环境变量列表,而 Repository::GitDependency#nw_fsck() 友好地将 Hash 返回给我们,因此我们可以公开传递给Rails的所有环境变量!

The Actual Impact 的实际影响

I inadvertently gained access to a total of 1220 environment variables (~2MB) containing a lot production access keys. I immediately stopped my testing, reached out to folks at GitHub to alerting them of this incident and proceeded to submit a vulnerability report soon after.
我无意中访问了总共1220个环境变量(~ 2 MB),其中包含大量生产访问密钥。我立即停止了测试,联系了GitHub上的人,提醒他们注意这一事件,并在不久后提交了一份漏洞报告。

Special thanks to Simon Gerst and Jorge Rosillo for their help in getting in contact with someone from the GitHub Security Incident Response Team (SIRT) team so that I could give them an early heads-up notice on this.
特别感谢Simon Gerst和Jorge Rosillo,他们帮助我联系了GitHub安全事件响应团队(SIRT)的人,这样我就可以给予他们一个关于这个问题的早期通知。

Getting Remote Code Execution (RCE)
获取远程代码执行(RCE)

The following day, I continued further research to see if it is possible to escalate the impact further and achieve remote code execution. Of course, not with the access keys! I didn’t want to mess with the production GitHub.com environment further, so I continued my research using my new test GHES setup.
第二天,我继续进一步研究,看看是否有可能进一步升级影响并实现远程代码执行。当然,不能用钥匙!我不想进一步干扰生产 GitHub.com 环境,所以我使用新的测试GHES设置继续我的研究。

I quickly noticed that the list of environment variables included ENTERPRISE_SESSION_SECRET, which is used for signing marshalled data stored in session cookies:
我很快注意到,环境变量列表中包括 ENTERPRISE_SESSION_SECRET ,它用于对存储在会话cookie中的编组数据进行签名:

As previously demonstrated in @iblue’s remote code execution in GitHub Enterprise management console, having knowledge of ENTERPRISE_SESSION_SECRET value allows an attacker to sign arbitrary serialised data.
正如之前在GitHub Enterprise管理控制台中@iblue的远程代码执行中所展示的那样,知道 ENTERPRISE_SESSION_SECRET 值允许攻击者对任意序列化数据进行签名。

Ruby on Rails implements session storage using a cryptographically signed serialized Ruby Hash. This Hash is serialized into a cookie using Marshal.dump and subsequently deserialized using Marshal.load. If an attacker can construct a valid signature, they can create a session cookie that contains arbitrary input passed to Marshal.load. As noted by the Ruby documentation for Marshal.load, this can result in code execution:
Ruby on Rails使用加密签名的序列化Ruby Hash 实现会话存储。这个Hash使用 Marshal.dump 序列化为一个cookie,然后使用 Marshal.load 序列化。如果攻击者可以构造有效的签名,他们就可以创建一个会话cookie,其中包含传递给 Marshal.load 的任意输入。正如Ruby文档中提到的 Marshal.load ,这可能会导致代码执行:

By design, ::load can deserialize almost any class loaded into the Ruby process. In many cases this can lead to remote code execution if the Marshal data is loaded from an untrusted source.
通过设计, ::load 可以将几乎任何加载到Ruby进程中的类进行并行化。在许多情况下,如果从不受信任的源加载Marshal数据,则这可能导致远程代码执行。

As a result, ::load is not suitable as a general purpose serialization format and you should never unmarshal user supplied input or other untrusted data.
因此, ::load 不适合作为通用序列化格式,并且您永远不应该对用户提供的输入或其他不受信任的数据进行解封。

This is a clear path to obtaining RCE, but there are still a few more hurdles to resolve (which I will discuss another time). However, this environment variable is not set on GitHub.com, so there’s no quick and easy way to get remote code execution on GitHub.com except to use the access keys (but don’t do this, please!).
这是获得RCE的明确途径,但仍有一些障碍需要解决(我将在另一个时间讨论)。但是,这个环境变量没有在 GitHub.com 上设置,所以除了使用访问键之外,没有快速简单的方法可以在 GitHub.com 上远程执行代码(但是请不要这样做!)。

To reiterate, this vulnerability can be chained to achieve RCE on GHES, but GitHub.com is not affected.
重申一下,可以链接此漏洞以在GHES上实现RCE,但 GitHub.com 不受影响。

Exploit Conditions 开发条件

This vulnerability affects GitHub.com and any GitHub Enterprise Server with GitHub Actions enabled. An attacker also needs to have the organisation owner role.
此漏洞影响 GitHub.com 和任何启用了GitHub操作的GitHub Enterprise Server。攻击者还需要具有组织所有者角色。

Suggested Mitigations 建议的缓解措施

  1. Validate the rid_key parameter in the Orgs::ActionsSettings::RepositoryItemsController class against an allowlist to ensure that only intended methods of the Repository class can be invoked.
    根据allowlist调用 Orgs::ActionsSettings::RepositoryItemsController 类中的 rid_key 参数,以确保仅可以调用 Repository 类的预期方法。
  2. Revoke and regenerate all secrets used in GitHub.com / any GitHub Enterprise Server that may have been compromised.
    撤销并重新生成 GitHub.com /任何可能已被泄露的GitHub Enterprise Server中使用的所有机密。
  3. Consider spawning git processes with a minimal set of environment variables required for functioning instead. Currently, a total of 1220 environment variables is being passed to the git process on GitHub.com.
    考虑使用运行所需的最小环境变量集来生成 git 进程。目前,共有1220个环境变量被传递到 GitHub.com 上的 git 进程。

Detection Guidance 检测指导

It is possible to detect the exploitation of this vulnerability by checking the server’s access logs for all requests made to /orgainzations/<organisation_name>/settings/actions/repository_items with an abnormal rid_key parameter value set. However, it is worthwhile to note that Rails accepts rid_key parameter supplied within the request body as well.
通过检查服务器的访问日志中是否有使用异常 rid_key 参数值对 /orgainzations/<organisation_name>/settings/actions/repository_items 发出的所有请求,可以检测到对该漏洞的攻击。然而,值得注意的是,Rails也接受请求体中提供的 rid_key 参数。

Timeline 时间轴

  • 2023-12-26 Vendor Disclosure
    2023-12-26供应商披露
  • 2023-12-26 Initial Vendor Contact
    2023-12-26初步供应商联系
  • 2023-12-26 Hotfixed on Github.com Production Servers
    2023-12-26 Github.com 生产服务器上的Hotfixed
  • 2023-12-28 Sent RCE Report to Vendor
    2023-12-28向供应商发送RCE报告
  • 2024-01-16 Vendor Patch & Announcement Release
    2024-01-16供应商补丁和公告发布
  • 2024-05-06 Public Release
    2024-05-06公开发布

Closing Thoughts 结束语

Regrettably, this vulnerability was discovered and exploited at a really inopportune time (day after Christmas). I want to express my sincere apologies and gratitude to all Hubbers involved and working during the Christmas/New Year festive period, since the amount of work created for the Hubbers as a consequence of this bug report must have been insanely huge. It is, however, incredibly impressive to see them get this vulnerability patched quickly, rotate all secrets (an awfully painful process), as well as running and concluding a full investigation to confirm that this vulnerability had not been exploited in-the-wild previously.
遗憾的是,这个漏洞在一个非常不合时宜的时间(圣诞节后的第二天)被发现和利用。我想向所有参与和在圣诞节/新年节日期间工作的Hubbers表示诚挚的歉意和感谢,因为这个错误报告为Hubbers创造的工作量肯定是巨大的。然而,令人印象深刻的是,他们迅速修补了这个漏洞,旋转了所有秘密(一个非常痛苦的过程),以及运行和结束了一个全面的调查,以确认这个漏洞以前没有被利用过。

I hope you have enjoyed reading the whole process on how I managed to exploit this inconspicuous, unsafe reflection bug with limited control and turning it one of the most impactful bugs on GitHub. Thanks for reading!
我希望你能享受阅读我如何利用这个不显眼的,不安全的反射错误的整个过程,并将其变成GitHub上最具影响力的错误之一。感谢您的阅读!

原文始发于@Creastery:Send()-ing Myself Belated Christmas Gifts – GitHub.com’s Environment Variables & GHES Shell

版权声明:admin 发表于 2024年5月6日 下午4:12。
转载请注明:Send()-ing Myself Belated Christmas Gifts – GitHub.com’s Environment Variables & GHES Shell | CTF导航

相关文章