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