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.
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
.
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.
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
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
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.
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?
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!
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.
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!