DEV Community

Anna Voronina
Anna Voronina

Posted on

Notepad injection or the story of writing new diagnostic rules

This article is about calling operating system commands in Java. Also, we'll cover OS command and argument injections, along with the process of writing diagnostic rules to detect such vulnerabilities.

1240_Notepade_Injection/image1.png

Diving deep

Hi to all readers! In this note, we'll explore the ways to call OS commands in Java and how their careless use may result in potential vulnerabilities. This post is a summary of the key insights that we gathered while developing diagnostic rules aimed at detecting potential vulnerabilities mentioned above.

We selected these rules for a reason. PVS-Studio Java analyzer is evolving into a full-fledged SAST solution. Therefore, we actively implement diagnostic rules that identify potential vulnerabilities. This way, we can meet the needs of clients with high security requirements to the software they develop.

OS command injection is a vulnerability that enables an attacker to execute malicious OS level commands on any machine. It becomes possible when an OS level command is partially or completely formed from external data.

The vulnerability is critical: in the OWASP Top Ten classification, it is categorized as A03 - Injections. It's essential to proactively find such issues.

Adventure time

Creating a diagnostic rule is always like an adventure. We start with exploring the subject area—in this case, OS commands execution via Java—and identifying various nuances that can complicate the process. Before creating a rule, we check if the analyzer is capable of handling it. If the analyzer lacks some features to support new rules, we take care of it above all else.

When developing the rule that detects OS command injections, we've gone through all I listed above. But let's take it one step at a time :)

Execution of OS commands in Java

There are 2 classes in the standard Java library for executing OS commands. Let's consider both.

Runtime

The first is Runtime. It enables a Java application to interact with the environment in which it is running.

To execute an OS command, we passed the required command to the exec method_._ Look at the example:

public class Main {
  public static void main(String[] args) throws IOException {
    Process process = Runtime.getRuntime().exec("ping 127.0.0.1");
    try {
      System.out.println(getProcessOutput(process));
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  private static String getProcessOutput(Process process) 
        throws IOException, InterruptedException {

    try (InputStream stream = process.getInputStream();
          BufferedInputStream inputStream = new BufferedInputStream(stream)
    ) {
      return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The ping 127.0.0.1 command runs, and the process outputs to the console. Here is the output snippet:

Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
....
Enter fullscreen mode Exit fullscreen mode

It all works—so far so good.

In more detail: the exec method takes a string—the OS command and its arguments. The method starts the OS process and returns the object of Process type containing information about it.

In getProcessOutput, we just get the output of the running process and convert it to a string.

Here is a very important point—the exec method uses the ProcessBuilder class under the hood. Let's break it down further.

ProcessBuilder

The ProcessBuilder class enables executing OS level commands. We can specify the command via the class constructor or using the command method. To start the process, we call the start method, which initiates process creation and returns the related Process object.

The use case:

public class Main {
  public static void main(String[] args) throws IOException {
    ProcessBuilder processBuilder = new ProcessBuilder("ping", 
                                                       "127.0.0.1");
    Process process = processBuilder.start();
    try {
      System.out.println(getProcessOutput(process));
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  // ....
}
Enter fullscreen mode Exit fullscreen mode

Compared to the previous example, only the first lines in the main method have changed, creating an object that runs the process we need. However, therein lies one interesting point—when passing the command to the ProcessBuilder constructor with parameters "in pieces", the first parameter is the command itself, followed by its arguments.

Based on this command, the start method initiates the process to be run.

Earlier, I mentioned that the exec method of the Runtime class uses ProcessBuilder under the hood. Now let's get down to the nuances.

Is it secure?

After learning about these methods, I wondered, "Is it possible to inject a malicious command to the main command if the command is formed from external data?".

So, I gave it a try:

public class Main {
  public static void main(String[] args) throws IOException {
    String notTaintedCommand = "ping ";
    String taintedData = args[0];
    Process process = Runtime.getRuntime().exec(notTaintedCommand + 
                                                taintedData);
    try {
      System.out.println(getProcessOutput(process));
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  // ....
}
Enter fullscreen mode Exit fullscreen mode

The elements of the args array represent the external data. In our synthetic example, its first element contains the string 127.0.0.1 & notepad.

We are expecting to receive an ip address to ping it. But the unfortunate "external data" is trying to ruin the whole picture. The plan was to ping some ip address after executing the command, followed by the "notepad injection". At least, this would happen in the cmd shell.

But not so fast. Here is the output:

Bad parameter &.

The ping utility alerted with this message.

The hitch is that the exec method splits the string into an array of strings using delimiter characters. This array is passed to the constructor of the ProcessBuilder instance being created. As mentioned above, ProcessBuilder creates a process based on the command that represents the first array element.

That is, the shell—cmd on Windows or bash on Unix systems—is not called. Because of this, shell metacharacters such as & or | are not interpreted as expected; instead, they are passed to the called program as separate parameters. That's the reason we've got this message.

If a command is formed from the external data only partially, it feels like the threat isn't that severe (unless the command is rm). Is it always the case? I will answer this question later, so stay tuned.

Diagnostic rule time

We've examined how OS commands are executed in Java. Now we can move on to diagnostic rule creation.

We thought it would be a simple task. Basically, it was supposed to be a simple diagnostic rule focusing on taint analysis.

To put in briefly, we've got:

  • taint sources, i.e. points where external data comes from;
  • tainted data;
  • taint sinks, i.e. execution points where tainted data may cause a vulnerability to be exploited;
  • sanitization, i.e. the process of checking / cleaning external data. It must be done before the data gets into the sink.

Our analyzer has a mechanism to detect when unchecked data gets to sinks unsanitized. We wrote two comprehensive articles about its implementation (click and click) and provided an overview in a general article (click).

At first, we planned to simply mark-up sources, sinks, and sanitizing methods. Command injection and argument injection prevented us from doing this.

Earlier, I mentioned that the command can be formed based on external data completely or partially. These cases are categorized as two different potential vulnerabilities:

  • Command injection (CWE-78)—the command and arguments are completely external.
  • Argument injection (CWE-88)—the command is predefined, only its parameters are external.

Why are they separated? The way I see it, their danger level is quite different. In the case of command injection, as hypothetical hackers, we can control the machine. In the case of argument injection, however, it depends on the circumstances.

The analyzer should distinguish these situations in order to issue different messages depending on what exactly happened.

In the case of ProcessBuilder, it's quite simple—we need to figure out the exact parameter with tainted data. If it is in the first parameter, it's a command injection. If not, it's argument injection.

The exec method is a bit more complicated—we pass it a single string, which contains both the command and the arguments. So, what shall we do?

The solution is quite trivial. We look at the formation of the string passed to exec. If it comes entirely from an external input, it's command injection. However, if a command is predefined, and the external string concatenates with it, we perceive it as argument injection.

Having overcome this difficulty, we tackled these diagnostic rules. Now we can catch things like this:

@RestController("/osInjection")
public class OSCommandController {

  @GetMapping("/execute")
  public String execute(@RequestParam("command")
                    String command) throws IOException {

    String taintedData = command;
    Process process = Runtime.getRuntime().exec(taintedData);
    try {
      return getProcessOutput(process);
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }


  // ....
}
Enter fullscreen mode Exit fullscreen mode

V5310 Possible command injection. Potentially tainted data in 'taintedData' variable is used to create OS command. OSCommandController.java 20

Also, we can catch the following:

@RestController("/os_injection")
public class OSCommandController {

  @GetMapping("/execute")
  public String execute(@RequestParam("argument")
                      String argument) throws IOException {

    String notTaintedData = "ping";
    String taintedData = argument;
    Process process = Runtime.getRuntime().exec(notTaintedData + " " + 
                                                taintedData);
    try {
      return getProcessOutput(process);
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  // ....
}
Enter fullscreen mode Exit fullscreen mode

V5311 Possible argument injection. Potentially tainted data in 'taintedData' variable is used to create OS command. OSCommandController.java 21

Woohoo!

Notepad injection

Now, let's get to the point mentioned in the article title. Remember I told you that processes are formed directly, not via a shell? Accordingly, one cannot really concatenate an additional malicious command to it.

Yes, initially, the case is the way it is. But as I said before, in the case of argument injection, the severity level depends on the circumstances.

What if we still want to use the OS shell to call commands? We still can. In the case of Windows, we should call the command cmd with /c as the first parameter. Then, the command we need will be executed via the cmd shell.

Since we're calling the command via the shell, can we use the concatenating shell metacharacters as well? Let's see:

public class Main {
  public static void main(String[] args) throws IOException {
    String notTaintedCommand = "cmd /c ping ";
    String taintedData = args[0];
    Process process = Runtime.getRuntime().exec(notTaintedCommand + 
                                                taintedData);
    try {
      System.out.println(getProcessOutput(process));
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }


  // ....
}
Enter fullscreen mode Exit fullscreen mode

Imagine that args[0] contains 127.0.0.0.1 & notepad. I ran it and guess what happened. The required ip address pinged and... the notepad actually opened.

This case shows that argument injection isn't that harmless. Calling a shell and passing it a command as an argument is the vivid example of those "circumstances" I mentioned above.

This case would seem to level out the distinction between the severity levels of command and argument vulnerabilities. Then why do we need two separate diagnostic rules for detecting them? The reasoning is as follows: calling the shell is a special case. Accordingly, the severity of argument injection depends on the "circumstances", or the context. As for command injection, it is always clear—the severity level is high regardless of the circumstances. So, the division into two separate diagnostic rules.

What if it's not that simple?

You may say that the above examples are rather naïve and you'd be right. Real-world cases may not be that obvious, at least when it comes to sources and sinks. We're aware of it and, therefore, will support user annotations in our Java analyzer in the nearest future. What's that?

Basically, it's a mechanism that enables you to "introduce" your code to the analyzer. As for taint diagnostic rules, you can explicitly indicate sources, sinks, or sanitizing methods. It will be implemented in a separate file where a user can mark it all up.

Certainly, we annotate classes from standard or most popular libraries ourselves. However, projects often rely on their own libraries, for example, to retrieve data. This way, the analyzer can meet client needs, ensuring more precise annotations.

Conclusion

We should always be cautious with external data.

Speaking of caution, I can't help but touch on an OWASP recommendation I liked. In their post regarding this vulnerability, their first piece of advice is simple: avoid calling any OS commands. After all, Java—and many other languages—are very likely to provide the necessary, higher-level tools for this task by default. Sounds reasonable. I too opt for reusing existing solutions rather than inventing new ones.

Also, I cannot help saying that now, since the 7.35 release, PVS-Studio Java analyzer has diagnostic rules that can help you detect the above potential vulnerabilities. If you want to try it, don't hesitate to follow the link.

That's it for me. Let's wrap things up here. See you soon!

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay