SQL Injection básico en JDBC
24 de mayo de 2016 • fjbalon
Buenos días. La siguiente entrada documenta cómo realizar una prueba de concepto que consiste en realizar una inyección SQL básico en un programa Java con JDBC (Java DataBase Connectivity). Además serán expuestas algunas posibles soluciones a esta vulnerabilidad. Para ello he creado una sencilla base de datos y he realizado un código de prueba en Java que consulta esta base de datos.
La base de datos simplemente se compondrá una tabla
Después, simplemente añadiremos varios usuarios con diferentes contraseñas. Podemos ver el script SQL:
CREATE DATABASE dbftwr CHARACTER SET utf8 COLLATE utf8_spanish2_ci;
CREATE TABLE Usuarios (
id INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
nombre TEXT NOT NULL,
pass TEXT NOT NULL,
secreto TEXT NOT NULL,
PRIMARY KEY (id));
INSERT INTO Usuarios (nombre,pass,secreto) VALUES
('pedro','123456','Pedro realmente es Pablo'),
('pablo','987654321','Pablo es el padre de Juan'),
('miguel','miguel','Miguel no tiene amigos');
Para realizar las consultas a la base de datos e iniciar sesión, he utilizado un código que genere una ventana gráfica (Swing) con un pequeño formulario de inicio de sesión:
package es.fwhibbit.sqli;
import java.awt.event.ActionEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
public class SQLInjection extends JFrame implements WindowListener {
private static final long serialVersionUID = 1L;
JLabel lblUsuario = new JLabel("Usuario: ");
JLabel lblPass = new JLabel("Pass: ");
JTextField txtUsuario = new JTextField("");
JTextField txtPass = new JTextField("");
JLabel lblSecreto = new JLabel("Secreto", SwingConstants.CENTER);
JButton btnAceptar = new JButton("Aceptar");
JButton btnLimpiar = new JButton("Limpiar");
public SQLInjection() throws ClassNotFoundException, SQLException {
setLayout(null);
setSize(400,250);
setTitle("SQL Injection Test");
setLocationRelativeTo(null);
setVisible(true);
add(lblUsuario); lblUsuario .setBounds(1, 1, 100, 50);
add(txtUsuario); txtUsuario .setBounds(100, 1, 300, 50);
add(lblPass); lblPass .setBounds(1, 60, 100, 50);
add(txtPass); txtPass .setBounds(100, 60, 300, 50);
add(lblSecreto); lblSecreto .setBounds(0, 120, 400, 50);
add(btnLimpiar); btnLimpiar .setBounds(95, 180, 100, 50);
add(btnAceptar); btnAceptar .setBounds(205, 180, 100, 50);
Class.forName("org.mariadb.jdbc.Driver");
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass);
Statement s = connection.createStatement();
addWindowListener(this);
// Botón cuya función es limpiar los campos de texto
btnLimpiar.addActionListener((ActionEvent event) -> {
txtUsuario.setText("");
txtPass.setText("");
});
btnAceptar.addActionListener((ActionEvent event) -> {
try {
ResultSet rs = s.executeQuery("select secreto from Usuarios where (nombre = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');");
rs.next();
lblSecreto.setText(rs.getString(1));
}
catch (SQLException e) {e.printStackTrace();}
});
}
public static void main(String[] args) throws NumberFormatException, IOException, ClassNotFoundException, SQLException {new SQLInjection();}
public void actionPerformed(ActionEvent ae) {}
public void windowActivated(WindowEvent we) {}
public void windowClosed(WindowEvent we) {}
public void windowClosing(WindowEvent we) {this.setVisible(false);}
public void windowDeactivated(WindowEvent we) {}
public void windowDeiconified(WindowEvent we) {}
public void windowIconified(WindowEvent we) {}
public void windowOpened(WindowEvent we) {}
}
Ejecutando el programa, podemos observar el siguiente resultado. Somos Pedro y conocemos su contraseña: 123456. Por lo que será nuestro conejillo de indias, de tal forma que tras iniciar sesión conoceremos el secreto de Pedro:
Los dos bloques de código que nos ocupan son los siguientes: el bloque de conexión a la base de datos y el contenido del botón de aceptar, que almacena la consulta.
Class.forName("org.mariadb.jdbc.Driver");
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass);
Statement s = connection.createStatement();
btnAceptar.addActionListener((ActionEvent event) -> {
try {
ResultSet rs = s.executeQuery("select secreto from Usuarios where (nombre = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');");
rs.next();
lblSecreto.setText(rs.getString(1));
}
catch (SQLException e) {e.printStackTrace();}
});
Como vemos la consulta
select secreto from Usuarios where (nombre = ' pedro ' and pass = ' 123456 ');
Esta sentencia mostraría el secreto de la tabla
select secreto from Usuarios where (nombre = ' pedro' or '1'='1 ' and pass = '');
De esta forma hemos injectado código: asignamos el nombre, es pedro (es true). O bien (
¿Cómo podemos solucionarlo? Con el fin de evitar este tipo de ataques sobre nuestro programa deberemos utilizar el método seguro: sustituir el
Class.forName("org.mariadb.jdbc.Driver");
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass);
//Statement s = connection.createStatement();
btnAceptar.addActionListener((ActionEvent event) -> {
try {
PreparedStatement ps = connection.prepareStatement("select secreto from Usuarios where (nombre=? and pass=?)");
ps.setString(1, txtUsuario.getText());
ps.setString(2, txtPass.getText());
ResultSet rs = ps.executeQuery();
rs.next();
lblSecreto.setText(rs.getString(1));
}
catch (SQLException e) {e.printStackTrace();}
});
Otra alternativa, también válida, para solventar esta vulnerabilidad, es hacer una única sentencia sin posibilidad de edición. Y a su vez jugar con las respuestas. El código será similar, de forma que:
Class.forName("org.mariadb.jdbc.Driver");
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass);
Statement s = connection.createStatement();
btnAceptar.addActionListener((ActionEvent event) -> {
try {
ResultSet rs = s.executeQuery("select * from Usuarios;");
while (rs.next()) {
if (rs.getString(2).equals(txtUsuario.getText()) && rs.getString(3).equals(txtPass.getText())) {
lblSecreto.setText(rs.getString(4));
break;
}
else {lblSecreto.setText("ERROR");}
}
}
catch (SQLException e) {e.printStackTrace();}
});
Como podemos ver, el bloque de conexión se mantiene intacto a la primera opción. El interior del botón simplemente tendrá la sentencia
De esta forma, al no modificar la sentencia directamente, sino jugar con sus resultados evitaremos esta vulnerabilidad en concreto. Así, hemos explotado y solucionado el riesgo que supone la vulnerabilidad de SQL Injection en nuestro software, evitando así posibles ataques a nuestra base de datos.