index.html (11017B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 6 <title>SQL Injection básico en JDBC</title> 7 <link rel="icon" type="image/png" href="/favicon.png" /> 8 <link rel="stylesheet" type="text/css" href="../article.css" /> 9 </head> 10 <body> 11 <header> 12 <p class="title">SQL Injection básico en JDBC</p> 13 <p class="date">24 de mayo de 2016 • fjbalon</p> 14 </header> 15 <p>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.</p> 16 <p>La base de datos simplemente se compondrá una tabla <emb>Usuarios</emb> que recoge cuatro atributos: (<emb>id</emb>), <emb>nombre</emb>, <emb>pass</emb> y <emb>secreto</emb>. 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.</p> 17 <p>Después, simplemente añadiremos varios usuarios con diferentes contraseñas. Podemos ver el script SQL:</p> 18 <pre><code>CREATE DATABASE dbftwr CHARACTER SET utf8 COLLATE utf8_spanish2_ci; 19 20 CREATE TABLE Usuarios ( 21 id INTEGER NOT NULL AUTO_INCREMENT UNIQUE, 22 nombre TEXT NOT NULL, 23 pass TEXT NOT NULL, 24 secreto TEXT NOT NULL, 25 PRIMARY KEY (id)); 26 27 INSERT INTO Usuarios (nombre,pass,secreto) VALUES 28 ('pedro','123456','Pedro realmente es Pablo'), 29 ('pablo','987654321','Pablo es el padre de Juan'), 30 ('miguel','miguel','Miguel no tiene amigos'); </code></pre> 31 <p>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:</p> 32 <pre><code>package es.fwhibbit.sqli; 33 34 import java.awt.event.ActionEvent; 35 import java.awt.event.WindowEvent; 36 import java.awt.event.WindowListener; 37 import java.io.IOException; 38 import java.sql.Connection; 39 import java.sql.DriverManager; 40 import java.sql.ResultSet; 41 import java.sql.SQLException; 42 import java.sql.Statement; 43 44 import javax.swing.JButton; 45 import javax.swing.JFrame; 46 import javax.swing.JLabel; 47 import javax.swing.JTextField; 48 import javax.swing.SwingConstants; 49 50 public class SQLInjection extends JFrame implements WindowListener { 51 private static final long serialVersionUID = 1L; 52 53 JLabel lblUsuario = new JLabel("Usuario: "); 54 JLabel lblPass = new JLabel("Pass: "); 55 JTextField txtUsuario = new JTextField(""); 56 JTextField txtPass = new JTextField(""); 57 JLabel lblSecreto = new JLabel("Secreto", SwingConstants.CENTER); 58 JButton btnAceptar = new JButton("Aceptar"); 59 JButton btnLimpiar = new JButton("Limpiar"); 60 61 public SQLInjection() throws ClassNotFoundException, SQLException { 62 63 setLayout(null); 64 setSize(400,250); 65 setTitle("SQL Injection Test"); 66 setLocationRelativeTo(null); 67 setVisible(true); 68 69 add(lblUsuario); lblUsuario .setBounds(1, 1, 100, 50); 70 add(txtUsuario); txtUsuario .setBounds(100, 1, 300, 50); 71 add(lblPass); lblPass .setBounds(1, 60, 100, 50); 72 add(txtPass); txtPass .setBounds(100, 60, 300, 50); 73 add(lblSecreto); lblSecreto .setBounds(0, 120, 400, 50); 74 add(btnLimpiar); btnLimpiar .setBounds(95, 180, 100, 50); 75 add(btnAceptar); btnAceptar .setBounds(205, 180, 100, 50); 76 77 Class.forName("org.mariadb.jdbc.Driver"); 78 String dbpass = ""; 79 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 80 Statement s = connection.createStatement(); 81 82 addWindowListener(this); 83 // Botón cuya función es limpiar los campos de texto 84 btnLimpiar.addActionListener((ActionEvent event) -> { 85 txtUsuario.setText(""); 86 txtPass.setText(""); 87 }); 88 89 btnAceptar.addActionListener((ActionEvent event) -> { 90 try { 91 ResultSet rs = s.executeQuery("select secreto from Usuarios where (nombre = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');"); 92 rs.next(); 93 lblSecreto.setText(rs.getString(1)); 94 } 95 catch (SQLException e) {e.printStackTrace();} 96 }); 97 98 } 99 public static void main(String[] args) throws NumberFormatException, IOException, ClassNotFoundException, SQLException {new SQLInjection();} 100 101 public void actionPerformed(ActionEvent ae) {} 102 public void windowActivated(WindowEvent we) {} 103 public void windowClosed(WindowEvent we) {} 104 public void windowClosing(WindowEvent we) {this.setVisible(false);} 105 public void windowDeactivated(WindowEvent we) {} 106 public void windowDeiconified(WindowEvent we) {} 107 public void windowIconified(WindowEvent we) {} 108 public void windowOpened(WindowEvent we) {} 109 }</code></pre> 110 <p>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:</p> 111 <img style="width: 300px;" src="jdbc1.png" /> 112 <p>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.</p> 113 <pre><code>Class.forName("org.mariadb.jdbc.Driver"); 114 String dbpass = ""; 115 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 116 Statement s = connection.createStatement(); 117 118 btnAceptar.addActionListener((ActionEvent event) -> { 119 try { 120 ResultSet rs = s.executeQuery("select secreto from Usuarios where (nombre = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');"); 121 rs.next(); 122 lblSecreto.setText(rs.getString(1)); 123 } 124 catch (SQLException e) {e.printStackTrace();} 125 });</code></pre> 126 <p>Como vemos la consulta <emb>select secreto from Usuarios where (nombre = '" + txtUsuario.getText() + "' and pass = '" + txtPass.getText() + "');</emb> 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:</p> 127 <pre><codeb>select secreto from Usuarios where (nombre = ' pedro ' and pass = ' 123456 ');</codeb></pre> 128 <p>Esta sentencia mostraría el secreto de la tabla <emb>Usuarios</emb>, donde el nombre sea <emb>pedro</emb> 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. <emb>True AND True = True</emb>: 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.</p> 129 </body> 130 <pre><codeb>select secreto from Usuarios where (nombre = ' pedro' or '1'='1 ' and pass = '');</codeb></pre> 131 <p>De esta forma hemos injectado código: asignamos el nombre, es pedro (es true). O bien (<emb>1=1</emb> -sentencia obviamente cierta, verdadera- y <emb>pass=''</emb> (cadena en blanco, obviamente falsa). <emb>True OR (True AND False) = True OR False = True</emb>. 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.</p> 132 <img style="width: 300px;" src="jdbc2.png" /> 133 <p>¿Cómo podemos solucionarlo? Con el fin de evitar este tipo de ataques sobre nuestro programa deberemos utilizar el método seguro: sustituir el <emb>Statement</emb> por el <emb>PreparedStatement</emb>. Vemos el bloque de código que hemos de editar: en primer lugar comentamos nuestro <emb>Statement</emb>, ya que no le daremos uso. Lo sustituimos por el <emb>PreparedStatement</emb>, donde le indicamos directamente la sentencia SQL; sustituyendo los elementos a insertar por signos de cerrar interrogación (?) (omitir comillas). Con la órden <emb>ps(PreparedStatement).setString</emb> 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 <emb>getText()</emb> como en mi caso.</p> 134 <pre><code>Class.forName("org.mariadb.jdbc.Driver"); 135 String dbpass = ""; 136 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 137 //Statement s = connection.createStatement(); 138 139 btnAceptar.addActionListener((ActionEvent event) -> { 140 try { 141 PreparedStatement ps = connection.prepareStatement("select secreto from Usuarios where (nombre=? and pass=?)"); 142 ps.setString(1, txtUsuario.getText()); 143 ps.setString(2, txtPass.getText()); 144 ResultSet rs = ps.executeQuery(); 145 rs.next(); 146 147 lblSecreto.setText(rs.getString(1)); 148 } 149 catch (SQLException e) {e.printStackTrace();} 150 });</code></pre> 151 <p>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:</p> 152 <pre><code>Class.forName("org.mariadb.jdbc.Driver"); 153 String dbpass = ""; 154 Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/dbftwr", "root", dbpass); 155 Statement s = connection.createStatement(); 156 157 btnAceptar.addActionListener((ActionEvent event) -> { 158 try { 159 ResultSet rs = s.executeQuery("select * from Usuarios;"); 160 while (rs.next()) { 161 if (rs.getString(2).equals(txtUsuario.getText()) && rs.getString(3).equals(txtPass.getText())) { 162 lblSecreto.setText(rs.getString(4)); 163 break; 164 } 165 else {lblSecreto.setText("ERROR");} 166 } 167 } 168 catch (SQLException e) {e.printStackTrace();} 169 });</code></pre> 170 <p>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 <emb>select * from Usuarios</emb>. 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.</p> 171 <img style="width: 70%;" src="jdbc3.png" /> 172 <p>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.</p> 173 </html>