• JDBC 及 sql注入问题


    一、相关概念

    1.什么是JDBC

      JDBC(Java Database Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。

    2.数据库驱动

      我们安装好数据库之后,我们的应用程序也是不能直接使用数据库的,必须要通过相应的数据库驱动程序,通过驱动程序去和数据库打交道。其实也就是数据库厂商的JDBC接口实现,即对Connection等接口的实现类的jar文件。

    二、常用接口

    1.Driver接口

      Driver接口由数据库厂家提供,作为java开发人员,只需要使用Driver接口就可以了。在编程中要连接数据库,必须先装载特定厂商的数据库驱动程序,不同的数据库有不同的装载方法。如:

      装载MySql驱动:Class.forName("com.mysql.jdbc.Driver");

      装载Oracle驱动:Class.forName("oracle.jdbc.driver.OracleDriver");

    2.Connection接口

      Connection与特定数据库的连接(会话),在连接上下文中执行sql语句并返回结果。DriverManager.getConnection(url, user, password)方法建立在JDBC URL中定义的数据库Connection连接上。

      连接MySql数据库:Connection conn = DriverManager.getConnection("jdbc:mysql://host:port/database", "user", "password");

      连接Oracle数据库:Connection conn = DriverManager.getConnection("jdbc:oracle:thin:@host:port:database", "user", "password");

      连接SqlServer数据库:Connection conn = DriverManager.getConnection("jdbc:microsoft:sqlserver://host:port; DatabaseName=database", "user", "password");

      常用方法:

      • createStatement():创建向数据库发送sql的statement对象。
      • prepareStatement(sql) :创建向数据库发送预编译sql的PrepareSatement对象。
      • prepareCall(sql):创建执行存储过程的callableStatement对象。
      • setAutoCommit(boolean autoCommit):设置事务是否自动提交。
      • commit() :在链接上提交事务。
      • rollback() :在此链接上回滚事务。

    3.Statement接口

      用于执行静态SQL语句并返回它所生成结果的对象。

      三种Statement类:

      • Statement:由createStatement创建,用于发送简单的SQL语句(不带参数)。
      • PreparedStatement :继承自Statement接口,由preparedStatement创建,用于发送含有一个或多个参数的SQL语句。PreparedStatement对象比Statement对象的效率更高,并且可以防止SQL注入,所以我们一般都使用PreparedStatement。
      • CallableStatement:继承自PreparedStatement接口,由方法prepareCall创建,用于调用存储过程。

      常用Statement方法:

      • execute(String sql):运行语句,返回是否有结果集
      • executeQuery(String sql):运行select语句,返回ResultSet结果集。
      • executeUpdate(String sql):运行insert/update/delete操作,返回更新的行数。
      • addBatch(String sql) :把多条sql语句放到一个批处理中。
      • executeBatch():向数据库发送一批sql语句执行。

    4.ResultSet接口

      ResultSet提供检索不同类型字段的方法,常用的有:

      • getString(int index)、getString(String columnName):获得在数据库里是varchar、char等类型的数据对象。
      • getFloat(int index)、getFloat(String columnName):获得在数据库里是Float类型的数据对象。
      • getDate(int index)、getDate(String columnName):获得在数据库里是Date类型的数据。
      • getBoolean(int index)、getBoolean(String columnName):获得在数据库里是Boolean类型的数据。
      • getObject(int index)、getObject(String columnName):获取在数据库里任意类型的数据。

      ResultSet还提供了对结果集进行滚动的方法:

      • next():移动到下一行   (注意:初始时,指针指向第一行的前一行,通过resultset.next()方法将游标移动到下一行,返回值是一个布尔类型。)
      • Previous():移动到前一行
      • absolute(int row):移动到指定行
      • beforeFirst():移动resultSet的最前面。
      • afterLast() :移动到resultSet的最后面。

    使用后依次关闭对象及连接:ResultSet → Statement → Connection

    三、使用JDBC的步骤

      加载JDBC驱动程序 → 建立数据库连接Connection → 创建执行SQL的语句Statement → 处理执行结果ResultSet → 释放资源

    1.注册驱动 (只做一次)

      方式一:Class.forName(“com.MySQL.jdbc.Driver”);
      推荐这种方式,不会对具体的驱动类产生依赖。
      方式二:DriverManager.registerDriver(com.mysql.jdbc.Driver);
      会造成DriverManager中产生两个一样的驱动,并会对具体的驱动类产生依赖。

    2.建立连接

     Connection conn = DriverManager.getConnection(url, user, password); 

      URL用于标识数据库的位置,通过URL地址告诉JDBC程序连接哪个数据库,URL的写法为:

      

      其他参数如:useUnicode=true&characterEncoding=utf8

    3.创建执行SQL语句的statement

    复制代码
    1 //Statement  
    2 String id = "5";
    3 String sql = "delete from table where id=" +  id;
    4 Statement st = conn.createStatement();  
    5 st.executeQuery(sql);  
    6 //存在sql注入的危险
    7 //如果用户传入的id为“5 or 1=1”,那么将删除表中的所有记录
    复制代码
    复制代码
    1  //PreparedStatement 有效的防止sql注入(SQL语句在程序运行前已经进行了预编译,当运行时动态地把参数传给PreprareStatement时,即使参数里有敏感字符如 or '1=1'也数据库会作为一个参数一个字段的属性值来处理而不会作为一个SQL指令)
    2 String sql = “insert into user (name,pwd) values(?,?)”;  
    3 PreparedStatement ps = conn.preparedStatement(sql);  
    4 ps.setString(1, “col_value”);  //占位符顺序从1开始
    5 ps.setString(2, “123456”); //也可以使用setObject
    6 ps.executeQuery(); 
    复制代码

    4.处理执行结果(ResultSet)

    1 ResultSet rs = ps.executeQuery();  
    2 While(rs.next()){  
    3     rs.getString(“col_name”);  
    4     rs.getInt(1);  
    5     //…
    6 }  

    5.释放资源

    复制代码
     //数据库连接(Connection)非常耗资源,尽量晚创建,尽量早的释放
    //都要加try catch 以防前面关闭出错,后面的就不执行了
    1 try { 2 if (rs != null) { 3 rs.close(); 4 } 5 } catch (SQLException e) { 6 e.printStackTrace(); 7 } finally { 8 try { 9 if (st != null) { 10 st.close(); 11 } 12 } catch (SQLException e) { 13 e.printStackTrace(); 14 } finally { 15 try { 16 if (conn != null) { 17 conn.close(); 18 } 19 } catch (SQLException e) { 20 e.printStackTrace(); 21 } 22 } 23 }
    复制代码

     四、什么是SQL注入??

             sql注入性问题:所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。

             SQL注入问题如何解决:采用预编译机制的PreparedStatement,在SQL上将参数通过占位符'?'来代替参数,另外通过setString形式将参数设置,PreparedStatement对象会分别将参数和SQL交给MySQL 服务器,执行之前会进行预编译,在预编译阶段会检测SQL是否正确,正确则将编译结果直接提交给SQL服务器执行,编译失败则直接返回,不在执行PreparedStatement。预编译语句java.sql.PreparedStatement ,扩展自 Statement,不但具有 Statement 的所有能力而且具有更强大的功能。不同的是,PreparedStatement 是在创建语句对象的同时给出要执行的sql语句。这样,sql语句就会被系统进行预编译,执行的速度会有所增加,尤其是在执行大语句的时候,效果更加理想。

             主要的优势如下:1、防止SQL注入

                                          2、使用预编译机制,执行效率是高于Statement

                                          3、sql语句中参数值是通过?形式来替代参数的,在使用PreparedStatement的setString方法给?赋对应的值,相对于SQL直接拼接更加安全

             PreparedStatement和Statement区别:1、语法不同:PreparedStatement可以使用预编译的SQL,Statement使用静态SQL

                          2、效率不同:PreparedStatement执行效率高于Statement

                             3、安全性不同:PreparedStatement可以防止SQL注入,Statement不能防止SQL注入

     代码举例:

    <%@ page language="java" import="java.util.*" pageEncoding="gb2312"%>
    <html>
    <body>
    <%
    //获得账号和密码
        String account=request.getParament("account");
        String password=request.getParament("password");
        if(account!=null){
          //验证账号和密码
            String sql="SELECT * FROM T_CUSTOMER WHRER ACCOUNT= '"+account+"' AND PASSWORD = '"+password+"' ";
    Out.println("数据库执行语句:<br>"+sql);
    }
    %>
    </body>
    </html>

          当输入的账号为:Java ,密码为:123时,

          上面代码运行结果为:SELECT * FROM T_CUSTOMER  WHRER ACCOUNT= 'java'   AND  PASSWORD = '123' ;

          从sql语法可以看出,该结果没有任何问题,数据库将对该输入进行验证并通过。

          但是该程序有漏洞!假如用户输入的账号为:Java ' or  1=1  --     ,   密码任意输入。

           此时代码运行结果为:SELECT * FROM T_CUSTOMER WHRER ACCOUNT= 'Java'  or  1=1  -- '  AND PASSWORD = '123';   (注意:-- (杠杠空格)表示注释,不会被运行)

                                             (单引号,用于结束前一条语句),(后边加上'-- ' 用于注释后边的语句

           若此时数据库对该sql进行验证时,因为“ 1=1” 永远成立,所以数据库将返回表中的信息。此时,网站就受到了sql注入的攻击。

     五,SQL注入练习

    (一)int型SQL注入

          try{
                Class.forName("com.mysql.jdbc.Driver");//1.加载JDBC驱动程序
               //2. 建立数据库连接Connection
                 Connection connection=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
               //3.创建执行SQL的语句Statement
                String id="1 or 1=1";
                String sql="delete from student where sid="+id;
                Statement statement=connection.createStatement();
               //4.处理执行结果ResultSet
                int re =statement.executeUpdate(sql);  //注意此时返回的是更新行数,所以用int
                System.out.println(re);
                //5.释放连接
                statement.close();
                connection.close();
            }catch (Exception e){
                System.out.println("连接失败");
            }            

    运行结果图:

           在运行时,因为sql语义被篡改,而导致系统检测到的语句是  delete from student where sname=1 or 1=1; 由于 1=1永远成立,所以此时系统就会将表中的所有记录都删除掉。

    因此为增加程序安全性,一般使用具有预编译功能的PreparedStatement接口。

    使用预编译的PreparStatement:

     此时若在使用  1=1 进行篡改语义时,就会报错。

        try{
                //1.加载JDBC驱动程序
                Class.forName("com.mysql.jdbc.Driver");
                //2. 建立数据库连接Connection
                Connection connection=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
                //3.创建执行SQL的语句PreparedStatement
                String sql="delete from cc where id=?";
                PreparedStatement statement=connection.prepareStatement(sql);//区别1
                statement.setInt(1,1);
                //4.处理执行结果ResultSet
                 int re =statement.executeUpdate();//区别2
                System.out.println(re);
                 //5.关闭连接
                statement.close();
                connection.close();
            }catch (Exception e){
                System.out.println("连接失败");
            }

    (二)string型SQL注入

        try{
                Class.forName("com.mysql.jdbc.Driver");//1.加载JDBC驱动程序
                Connection connection=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
                //2. 建立数据库连接Connection
                String s="wulan' or 1=1-- ";  (注意此处是杠杠空格)
                String sql="select * from student where sname= '"+s+"'";
                Statement statement=connection.createStatement();//3.创建执行SQL的语句Statement
                ResultSet re =statement.executeQuery(sql);//4.处理执行结果ResultSet
               while (re.next()){
                    int str1=re.getInt(1);
                    String str2=re.getString(2);
                    int str3=re.getInt(3);
                    String str4=re.getString(4);
                    System.out.println(str1+"  "+str2+"  "+str3+"  "+str4);
                }
                re.close();//5.关闭连接
                statement.close();
                connection.close();
            }catch (Exception e){
                System.out.println("连接失败");
            }

    通过bug可知sql被执行后,传入系统检测时的形式:  (注意杠杠后面有空格

     相当于系统执行的是: select * from student where sname='wulan' or 1=1;

     执行结果:

    使用预编译:

    具有sql注入时的执行:

    try{
    Class.forName("com.mysql.jdbc.Driver");//1.加载JDBC驱动程序
    Connection connection=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
    //2. 建立数据库连接Connection
    String sql="select * from student where sname= ?";
    PreparedStatement statement=connection.prepareStatement(sql);//3.创建执行SQL的语句Statement
    statement.setString(1,"wulan' or 1=1-- ");
    ResultSet re =statement.executeQuery();//4.处理执行结果ResultSet
    while (re.next()){
    int str1=re.getInt(1);
    String str2=re.getString(2);
    int str3=re.getInt(3);
    String str4=re.getString(4);
    System.out.println(str1+" "+str2+" "+str3+" "+str4);
    }
    re.close();//5.关闭连接
    statement.close();
    connection.close();
    }catch (Exception e){
    System.out.println("连接失败");
    }

    运行结果图:

    没有sql注入时的执行:

    try{
        Class.forName("com.mysql.jdbc.Driver");//1.加载JDBC驱动程序
        Connection connection=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","123456");
        //2. 建立数据库连接Connection
        String sql="select * from student where sname= ?";
        PreparedStatement statement=connection.prepareStatement(sql);//3.创建执行SQL的语句Statement
        statement.setString(1,"wulan");
        ResultSet re =statement.executeQuery();//4.处理执行结果ResultSet
        while (re.next()){
            int str1=re.getInt(1);
            String str2=re.getString(2);
            int str3=re.getInt(3);
            String str4=re.getString(4);
            System.out.println(str1+"  "+str2+"  "+str3+"  "+str4);
        }
        re.close();//5.关闭连接
        statement.close();
        connection.close();
    }catch (Exception e){
        System.out.println("连接失败");
    }

    运行结果:

     六. 测试有没有注入漏洞 

      (一)整型参数的判断        

        ① http://www.19cn.com/showdetail.asp?id=49
        ② http://www.19cn.com/showdetail.asp?id=49 and 1=1
        ③ http://www.19cn.com/showdetail.asp?id=49 and 1=2

      这就是经典的1=1、1=2测试法了,怎么判断呢?看看上面三个网址返回的结果就知道了:

      可以注入的表现:

        ① 正常显示(这是必然的,不然就是程序有错误了)
        ② 正常显示,内容基本与①相同
        ③ 提示BOF或EOF(程序没做任何判断时)、或提示找不到记录(判断了rs.eof时)、或显示内容为空(程序加了on error resume next)

       不可以注入就比较容易判断了,①同样正常显示,②和③一般都会有程序定义的错误提示,或提示类型转换时出错。

     (二)字符串型参数的判断

    ???????????????????

     七 .判断数据库类型

       不同的数据库的函数、注入方法都是有差异的,所以在注入之前,我们还要判断一下数据库的类型。

      怎么让程序告诉你它使用的什么数据库呢?

         SQLServer有一些系统变量,如果服务器IIS提示没关闭,并且SQLServer返回错误提示的话,那可以直接从出错信息获取,方法如下: http://www.19cn.com/showdetail.asp?id=49 and user>0

      让我看来看看它的含义:首先,前面的语句是正常的,重点在and user>0,我们知道,user是SQLServer的一个内置变量,它的值是当前连接的用户名,类型为nvarchar。拿一个nvarchar的值跟int的数0比较,系统会先试图将nvarchar的值转成int型,当然,转的过程中肯定会出错,SQLServer的出错提示是:将nvarchar值 ”abc” 转换数据类型为 int 的列时发生语法错误,呵呵,abc正是变量user的值,这样,不废吹灰之力就拿到了数据库的用户名。

    补充:SQLServer的用户sa是个等同Adminstrators权限的角色,拿到了sa权限,几乎肯定可以拿到主机的Administrator了。上面的方法可以很方便的测试出是否是用sa登录,要注意的是:如果是sa登录,提示是将”dbo”转换成int的列发生错误,而不是”sa”。

        如果服务器IIS不允许返回错误提示,那怎么判断数据库类型呢?(举例:Access和SQLServer)

             我们可以从Access和SQLServer和区别入手,Access和SQLServer都有自己的系统表,比如存放数据库中所有对象的表,Access是在系统表[msysobjects]中,但在Web环境下读该表会提示“没有权限”,SQLServer是在表[sysobjects]中,在Web环境下可正常读取。

    在确认可以注入的情况下,使用下面的语句:

    http://www.19cn.com/showdetail.asp?id=49 and (select count(*) from sysobjects)>0
    http://www.19cn.com/showdetail.asp?id=49 and (select count(*) from msysobjects)>0

       如果数据库是SQLServer,那么第一个网址的页面与原页面http://www.19cn.com/showdetail.asp?id=49是大致相同的;而第二个网址,由于找不到表msysobjects,会提示出错,就算程序有容错处理,页面也与原页面完全不同。

       如果数据库用的是Access,那么情况就有所不同,第一个网址的页面与原页面完全不同;第二个网址,则视乎数据库设置是否允许读该系统表,一般来说是不允许的,所以与原网址也是完全不同。大多数情况下,用第一个网址就可以得知系统所用的数据库类型,第二个网址只作为开启IIS错误提示时的验证。

     

    摘自:JDBC

               SQL注入式问题

  • 相关阅读:
    Core Animation 文档翻译—附录C(KVC扩展)
    Core Animation 文档翻译—附录B(可动画的属性)
    Core Animation 文档翻译—附录A(Layer样貌相关属性动画)
    Core Animation 文档翻译 (第八篇)—提高动画的性能
    Core Animation 文档翻译 (第七篇)—改变Layer的默认动画
    Core Animation 文档翻译 (第六篇)—高级动画技巧
    Core Animation 文档翻译 (第五篇)—构建Layer的层次结构
    用Markdown快速排版一片文章
    Core Animation 文档翻译 (第四篇)—让Layer的content动画起来
    Core Animation 文档翻译(第三篇)—设置Layer对象
  • 原文地址:https://www.cnblogs.com/ljl150/p/12045942.html
Copyright © 2020-2023  润新知