Basic SQL Injection on JDBC

Colaborador: Hackbier

¡Buenos días!. La siguiente entrada documenta cómo realizar un SQL Injection 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.

 

Creación de la base de datos SQL

La base de datos simplemente se compondrá una tabla, llamada ‘Usuarios’ que recoge cuatro atributos: un identificador (id), un nombre de usuario (nombre), una contraseña (pass) y un secreto (secreto). En este último almacenaremos una información ‘secreta’ de cada usuario, que sólo podrá verla este usuario una vez inicie sesión. Tanto la contraseña como el secreto se encuentran en texto plano, siendo lo ideal cifrarlos para darle seguridad. Pero al tratarse de una prueba básica lo dejaremos así para verlo con más facilidad.

Después simplemente añadiremos varios usuarios con diferentes contraseñas. Podemos ver el script SQL:

/*  Creamos la base de datos  */
CREATE DATABASE dbftwr CHARACTER SET utf8 COLLATE utf8_spanish2_ci;

/*  Creamos las tablas  */
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));

/* Creamos los inserts */
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'); 

 

Uso vulnerable

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. Podemos observar el código aquí:

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;
	
	// Elementos gráficos
	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 {
		
		// Configuración general de la ventana
		setLayout(null);
		setSize(400,250);
		setTitle("SQL Injection Test");
		setLocationRelativeTo(null);
		setVisible(true);
		
		// Añadimos los elementos a la ventana
		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);

		// Bloque de conexión a la base de datos
		Class.forName("org.mariadb.jdbc.Driver"); 
		String dbpass = "";
		Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 
		Statement s = connection.createStatement();
		
		// Listeners
		addWindowListener(this);
		// Botón cuya función es limpiar los campos de texto
		btnLimpiar.addActionListener((ActionEvent event) -> {
        	txtUsuario.setText("");
        	txtPass.setText("");
        });
		// Botón para iniciar sesión, está sección de código será la encargada de realizar las consultas
		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();}
        });
		
		//connection.close();
	}
	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 forma que iniciando sesión correctamente, nos devolverá 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.

// Bloque de conexión a la base de datos
Class.forName("org.mariadb.jdbc.Driver"); 
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 
Statement s = connection.createStatement();


// Botón para iniciar sesión, está sección de código será la encargada de realizar las consultas
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 = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');

Es vulnerable a un SQL Injetion, ya que al almacenar las variables directamente en la sentencia SQL, enviará la cadena de texto tal y como lo recoja de los campos de texto. Por lo que podemos jugar. Una consulta típica, siguiendo el ejemplo de pedro, sería:

select secreto from Usuarios where (nombre = ' pedro ' and pass = ' 123456 ');

Esta sentencia mostraría el secreto de la tabla Usuarios, donde el nombre sea pedrro Y su contraseña 123456. Con esto comprobamos que la contraseña corresponda al usuario. Al ser nombre igual a pedro devolverá un booleano true (verdadero); al ser la contraseña 123456 también devolverá un booleano true. True AND True = True: Sesión iniciada correctamente. Pero ¿y si insertamos código SQL, para engañar al servidor y que nos muestre el secreto de quien queramos, sin necesidad de conocer su contraseña?. Esto lo podemos hacer aprovechando esta vulnerabilidad.

select secreto from Usuarios where (nombre = ' pedro' or '1'='1 ' and pass = '');

De esta forma hemos injectado código (en rojo). De esta forma asignamos el nombre, es pedro (es true). O bien (1=1 (sentencia obviamente cierta, verdadera) Y pass=» (cadena en blanco, obviamente falsa)). True OR (True AND False) = True OR False = True. Obtenemos finalmente un booleano verdadero, sin conocer la contraseña, lo que nos devolverá el secreto de este usuario. Podemos realizar una prueba con el usuario Miguel, el cual no conocemos su contraseña.

 

Uso seguro

¿Cómo podemos solucionarlo? Con el fin de evitar este tipo de ataques sobre nuestro programa deberemos utilizar el método seguro: sustituir el Statement por el PreparedStatement. Vemos el bloque de código que hemos de editar: en primer lugar comentamos nuestro Statement, ya que no le daremos uso. Lo sustituimos por el PreparedStatement, donde le indicamos directamente la sentencia SQL; sustituyendo los elementos a insertar por signos de cerrar interrogación (?) (omitir comillas). Con la órden ps(PreparedStatement).setString y el número de posición del elemento que hemos colocado en la sentencia, indicaremos el valor a dar, ya sea una variable o un getText() como en mi caso.

// Bloque de conexión a la base de datos
Class.forName("org.mariadb.jdbc.Driver"); 
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 
//Statement s = connection.createStatement();


// Botón para iniciar sesión, está sección de código será la encargada de realizar las consultas
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();}
});

De esta forma evitaremos que nuestro código sea vulnerable a ataques del tipo SQL Injection, al menos a los más conocidos. Ya sabéis, ¡nunca estaremos 100% seguros!.

 

Uso seguro (alternativa)

Una forma 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:

// Bloque de conexión a la base de datos
Class.forName("org.mariadb.jdbc.Driver"); 
String dbpass = "";
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 
Statement s = connection.createStatement();


// Botón para iniciar sesión, está sección de código será la encargada de realizar las consultas
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 «select * from Usuarios» (selecciona todo de la tabla Usuarios). De los resultados que obtenga, mientras haya irá recorriendo de uno en uno, comprobando que el contenido del campo de texto de usuario sea el mismo que el nombre de usuario de la tabla, Y el contenido del campo de texto de la contraseña sea igual a la contraseña del usuario de la tabla. Si estas condiciones se cumplen, se mostrará el secreto, sino, mostrará un error.

De esta forma, al no modificar la sentencia directamente, sino jugar con sus resultados evitaremos esta vulnerabilidad en concreto.

De esta forma tan sencilla, hemos explotado y solucionado la vulnerabilidad de SQL Injection básica en nuestro software. Esta inyección, reitero que es básica, existen otras compuestas con más potencial y complejidad, pero esas, las dejo para otra entrada… Espero que os haya gustado, ¡un saludo!.

4 comentarios en «Basic SQL Injection on JDBC»

  1. no quiero pasarme de ignorante, pero serian las mismas bases como para vulnerar un portal cautivo; es posible?

    1. En principio sí, por supuesto. El portal cautivo suele utilizar PHP para sus consultas a la base de datos, pero el método de SQL Injection es prácticamente igual. Buscando en Internet encontrarás ataques más específicos. Un saludo.

Los comentarios están cerrados.