3.4 Exception Handling Exercise

1. Introduction

Exceptions are a control flow mechanism used to handle and propagate errors. Some form of these mechanisms exist in nearly every modern programming language. When a programmer writes code that may throw an exception, improper handling of that exception can cause the program to crash (effecting a denial of service), reveal information that is valuable to an attacker, or result in partially changed state (where the changes made immediately before the error are not reverted). See the chapter on Exceptions for more details on exception-related vulnerabilities.

1.1 Exercise Description

In this exercise, we provide a very simple program that checks for a given username and password in a portable SQLite database. If a correct username and password is given, then the program will display a welcome message. If an incorrect password or nonexistent username is supplied, the program will display a failure message. Your objective is to trigger exceptions in several ways to cause the program to crash, cause the program to leak information about our database access, and cause the program to trigger a "correct" login without credentials.

Note: This program violates several best practices regarding password storage, credential management, authentication, etc., but you should focus on the exceptions for this exercise. You will see this sample again in 3.8.1 SQL Injections.

1.2 Vulnerability Mitigation

Proper exception handling includes many best practices. For this exercise, we will add conditions to handle exceptions that would crash the program, remove details from our error messages, and change the logic of our login method to appropriately deny input that causes an exception. The chapter on Exceptions covers more of these mitigation practices.

2. Exercise Instructions

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

cd 3.4_exceptions

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

If you have edited the Main.java file and it contains compiler errors, the make command will fail and show you where the errors were found.

2.2 Run the Program

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

java Main

You should see output from the program prompting for a username. Type a username and press enter. The program will then prompt you for a password. Type a password and press enter. To ease exploitation, the password field will not be hidden. The program will check the SQLite database for the username/password combination and tell you if the login was successful. The following is an example of a correct username/password input:

username: some_guy
password: his_password
Login Successful! Welcome some_guy

Try this a few times with different usernames and passwords to see how the program behaves. You may even want to try some inputs that you think might break the program, but we will focus on this more later. To exit the program, type exit in place of a username. All of the "correct" username and password combinations can be found in create_db.sql, which the Makefile uses to generate the database.

2.3 Inspect the Program Code

Now that you understand the basic behavior of the program (and maybe even an input that breaks it), you should take a look at the implementation 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

Look through the code first to understand the basic control flow and logic. Once you can follow the program logic, focus on the checkPW method to understand the purpose of the catch blocks. Begin thinking about what exceptions the program might not be handling correctly or at all. An important note to remember is that Unchecked Exceptions do not need to be "caught" whereas Checked Exceptions (like the SQLException) must be within a try-catch block.

After looking at the code, you might be thinking that you will need to trigger a SQLException. You would be right. Find an attack vector from the user's input at the terminal to one of the method calls that can throw a SQLException. Specifically, find which method within the try block has an argument containing the user's input.

2.4 Exploit the Vulnerability

Now that you know where your input can reach the exception-throwing code, it is time to cause an exception. Run the program again and test inputs that you think will cause an exception.

When you successfully trigger a SQLException, you will see that something else happens. Congratulations, you've exploited an exception handling vulnerability to crash the program and reveal sensitive information about the SQL query! If this was a network service, you would have caused a successful denial of service attack. The information you revealed about the SQL query will be very useful for a potential SQL Injection attack.

Before continuing to the mitigation, inspect the code again to understand why we got a second Exception in addition to the SQLException that we were expecting. Explain which is that second Exception, and why it is generated.

2.5 Mitigate the Vulnerability

Your exploit above revealed two problems with exception handling in this program: an unhandled Exception that crashes the program and a SQLException that reveals information about the program and database structure.

First, mitigate the Exception different than SQLException problem.

Next, you will mitigate the information leak in our SQLException handling. It is very common for programmers to accidentally leave sensitive debugging information in production code. Before mitigating the vulnerability in this exercise, we will cover some background on error logging. The best practice is to log errors in a safe place using some logging framework like Log4j. This may be a secure log file, a remote logging server, or some other safe system for recording the information. An important note is to never log private user information like passwords, personally identifiable information, credit card numbers, etc. Instead, you should log the relevant program state. The user will never see this logged information. Instead, you should display a generic error message to the user. In more advanced systems, you may even include a reference number (also recorded in the log file) that a user can give to a support team to find details about the error. For more information on related practices, see the chapter on Exceptions. For this exercise, there is no logging framework, so you can simply report the generic error to the user on the error stream.

Implement these fixes in Main.java. Do not forget to compile the program using the make command every time you change the source file. Run the program with the java Main command and test your implementation. First test that the correct program behavior is unchanged (i.e., correct username and password combinations succeed and incorrect username and password combinations fail). You may have to repeat this process several times to get the program to compile and run correctly. Now test your previous exploit input. You should see the generic error message that you report in the SQLException catch block when you are successful.

Now there's another problem! Your output should also show that the login was successful! Once again, you are experiencing the tendency of error handling problems to cascade, hiding and revealing other errors. This is a third problem with error handling that your exploit only revealed after fixing a previous problem.

Implement the fix for this third problem and repeat the same testing procedure as before. This time, continue until none of your inputs cause a successful login message except correct username and password combinations. Note that a SQL Injection is still possible, but you can ignore this until the injection exercise.

Congratulations! You have successfully mitigated an improper exception handling vulnerability!