3.8.2 Command Injection Exercise

1. Introduction

Command injections are possible when user input is interpreted by a system shell (e.g., /bin/sh, Windows Command Prompt, or PowerShell). In the worst case, an attacker can execute arbitrary commands to execute other programs, elevate privilege, and access other system resources.

In Java programs, this vulnerability often appears in the form of a call to Runtime.exec("cmd /C" ...) or Runtime.exec("/bin/sh -c" ...) where unsanitized user input is used as arguments to some external program or non- executable command.

1.1 Exercise Description

In this exercise, we provide a naive program that looks for a domain name address. This simple example demonstrates how a developer might choose to use an external program to perform certain tasks. When an external program does not have a compatible API, it is tempting to use a shell interpreter to invoke the program, but this may expose the risk of command injection. Exploit the command injection vulnerability to run an extra command. In a real exploit, the command may be something disastrous like rm -rf /, but for now stick with something more innocuous like cat /etc/passwd.

1.2 Vulnerability Mitigation

For this exercise, we will see two ways to mitigate the vulnerability. The first will "Eliminate the Shell", which is an important part of the attack vector. By executing a program directly instead of trying to run a shell command, we mitigate the possibility of abusing shell metacharacters to execute two commands.

Even better than invoking another program is using an internal API. In this example, the JDK has internal implementations of domain name resolution. Using an API instead of executing a program provides much more control over how the library/program interprets inputs. For example, a well-written API will have separate interfaces/methods for separate functionality, so you will not be able to accidentally allow illegal behavior. In nearly every case, an internal API is both more secure and more efficient than trying to execute an external program.

2. Exercise Instructions

This exercise will be completed entirely on the command line shell of the provided virtual machine. To open the shell, right-click on the "EXERCISES" directory and select "Open in Terminal". Enter the following command to change into the Command Injection exercise directory:

cd 3.8.2_command_injections

2.1 Compile the Program

We provide a Makefile that will compile the program. Every time you change the Main.java file, you must recompile the program before running it again. Enter the following command to compile the program:

make

2.2 Run the Program

The next step is to run the program and test some inputs. To execute the program, enter the following command:

java Main

You should see the output from the program prompting for a hostname. Type a hostname like wisc.edu and press enter. The program will lookup the hostname using the nslookup command and display the output. Here is an example of a successful input/output for the program:

hostname to lookup: wisc.edu
Server:		127.0.1.1
Address:	127.0.1.1#53

Non-authoritative answer:
Name:	wisc.edu
Address: 13..92.9.70

Try this with a few different hostnames, including hostnames that do not exist or are not correctly formatted. To exit the program, simply type exit in place of a hostname. On some inputs, it is possible for the nslookup command to execute in "interactive mode" when no arguments are provided to it. In this case you can press ctrl+c to terminate the program.

2.3 Inspect the Program Code

Now that you understand the basic behavior of the program, it's time to look at the implementation. This program is implemented in Main.java. Use your favorite text editor to open this file. Enter the following command to open the file in Nano:

nano Main.java

Spend some time looking at the code and tracing the flow of execution. You're looking for an attack surface and corresponding attack vector. In other words, how can an attacker's input reach the shell command?

2.4 Exploit the Vulnerability

Exit the text editor. Run the program again in the same way as Run the Program. Enter a hostname that, when inserted into the shell interpreter arguments, will result in a second command being run.

Once you manage to see the output of your second command, it's time to fix the vulnerability!

2.5 Mitigate the Vulnerability

Let's go back to our text editor and open Main.java (see Inspect the Program Code). This time, we're going to make some changes. The vulnerability in this example comes from the shell interpreter's ability to execute multiple programs. Instead of executing a shell command (e.g., /bin/sh), execute the intended program directly.

Once you have a potential fix implemented, save and exit the file (e.g., in Vim, press esc then type :wq). Now recompile and run the program as we did in Compile the Program and Run the Program. Try your exploit again. Make sure to test both good and bad inputs, so that we know the exploit is no longer possible and the program still works as intended.

Repeat the process of changing the program, compiling, and testing until you are convinced that the vulnerability is mitigated and the original program intent remains functional.

Improve the Mitigation using an Internal API

Now we'll improve our solution by using an internal API. This will remove the possibility of bad input to the nslookup command causing unexpected behavior. Look at java.net.InetAddress in the JDK documentation for domain name lookup APIs. Use the documentation to create a new method that replaces rDomainName() and generates the appropriate output using java.net.InetAddress.

Repeat the process of changing the program, compiling, and testing until you are convinced that the vulnerability is mitigated and the original program intent remains functional. This time, you should find that the program will not be easily broken!