一般来讲,刚入行的开发者容易忽视这个问题,随着经验的累积和良好开发习惯的养成,成熟的开发工程师写的代码里很少会有SQL注入攻击的错误。
SQL注入攻击是一种比较常见的攻击方式,其原理也比较简单:就是利用系统在执行数据库操作的环节,通过构造出特定参数,改变执行数据库操作SQL语句的逻辑和功能,达到攻击的目的。举个最简单也最容易理解的例子,如登录系统时,在后台做用户口令的验证这样写:
"select id from users where username =
‘"+USERNAME +"’ and password = ‘" + PASSWORD +"’" ,其中USERNAME和PASSWORD都是从网页表单获得的数据。假设攻击者知道有用户名为’Administrator’,但不知道口令,那可以在USERNAME输入Administrator’–,PASSWORD送不输入,则上述SQL语句将变成:
“select id from users where username = ‘Administrator’–’ and password = ‘xxx’”,password字段的验证被屏蔽掉了,SQL语句返回了Administrator用户的id,系统就会认为当前登录用户是Administrator。如果不知道用户名,也没有关系,在网页表单中将PASSWORD输入为’ or ‘1’='1,USERNAME随便输入,则上述SQL语句将变成"
“select id from users where username = ‘XXXX’ and password = ‘’ or ‘1’=‘1’”,因为’1’='1’永远成立,所以这条语句会返回users表中所有用户 的id,也可以达到绕过系统验证的目的。
更进一步,如果使用的是MYSQL数据库,USERNAME输入为’ ;DROP DATABASE (DB Name) --,则SQL语句将变成:
“select id from users where username = ‘’ ;DROP DATABASE (DB Name) --’ and password = ‘xxx’”,不出意外地话,这条SQL语句将删除指定的数据库。
由此可见,SQL攻击就是利用了程序直接拼接SQL语句,而且对用户输入不做任何检查的漏洞。
一般来讲,SQL注入攻击分为下面三步:
1.寻找到SQL注入的位置
首先分析web网站URL里的参数。一般来说,SQL注入一般存在于形如:HTTP://xxx.xxx.xxx/abc.asp?id=XX等带有参数的网页中,里面的参数id很有可能是进行SQL攻击的突破点。然后为参数赋不同的值,如id=XX’;id=XX and1=1;id=XX and 1=2;然后根据执行结果判断是否存在DQL攻击。
2.判断服务器类型和后台数据库类型
不同的数据库有不同的SQL语法,这对编写有效的SQL攻击很有意义。可以通过在参数中构造一些特定的函数注入,根据后台数据库是否支持,以判断数据库类型。也可以有意构造错误的注入,利用系统错误回显信息,进行数据库判断。
3.针对不同的服务器和数据库特点进行SQL注入攻击
到这一步,就是八仙过海,各显神通了。攻击者根据上面两步摸排的情况及攻击目的,制定不同的方案和实施路线。都过设置不同的注入语句和多次的攻击尝试,攻击者可以把数据库的重要信息摸得一清二楚,包括数据库名、表名、表结构等等,并且可以对数据进行编辑修改。在MS SQL Server里,攻击者甚至可以通过尝试运行内置存储过程xp_cmdshell来获得管理员权限。
在上述过程中,可能会经常使用到盲注。盲注就是指在注入过程中,SQL语句执行后预期的数据不能回显到前端页面,因此需要利用一些方法进行判断或者尝试,比如分析返回的错误页面,或比较不同的返回结果。
另外还有一种情况,攻击者不是通过执行注入语句的返回结果,而是通过执行语句的时间来判断注入攻击是否成功。这种方式也属于盲注,叫Timing Attack。
SQL注入产生的根本原因,和缓冲区溢出类似,就是用户输入的数据,变成了代码被执行。因此,解决的方法也是从这里入手,严格划分数据与代码的边界。
1.使用参数化语句
一定不要像上文中的例子那样直接用用户输入的参数拼接SQL语句,而是用参数化语句,具体方法有两种:第一种是预编译语句。预编译语句原本是为了加快SQL执行效率,它将SQL语句中固定的部分编译成可执行代码,变化的部分由调用程序作为参数传入,这样在做批量执行同一SQL语句时不用重复之前的语义检查等操作,直接执行即可。也正因为如此,传入的参数真的只能作为参数处理了,不会再被当做语句内容执行了。如下面的代码:
PreparedStatement pstmt = conn.prepareStatement(sql);
for(int i =0;i pstmt.setObject(i+1, params[i]); } rs = pstmt.executeQuery(); 这里需要注意的是,一定要用预编译对象的接口设置参数,不能直接拼接SQL语句,否则等于没有做任何防护。 第二种方法是使用存储过程,基本道理与使用预编译一样。不过同样注意的是,调用存储过程时参数应该用接口来设置,如SQL Server自带的Parameters集合。存储过程里也是直接用参数,而不要再拼接SQL语句。即应该写成这样: @name … select id from users where username = @name 而不是 set @sql=“select id from users where username =’”+[@name]+"’" exec(@sql) 2.用户输入检查 程序应该严格检查用户输入的数据,包括但不限于长度、类型、有无含有特殊字符,特殊字符就是在SQL语句中有意义的字符,它是完成注入攻击的主要帮凶,如分号、单双引号、等于号、注释等等。通过类型、长度、格式和范围来验证用户输入,过滤用户输入的内容。这是防止SQL注入式攻击的常见并且行之有效的措施。而且这个检查应该在客户端、服务器端、数据层多层次进行,最大可能减少注入攻击的可能。 3.不要返回数据库错误信息 不直接返回数据库操作时错误的结果,会增加SQL注入攻击的难度。 4.限制数据库用户权限 将用于数据库运行的用户账户权限设置的尽可能低,以限制攻击者在设法获取访问权限时可以执行的操作。 5.其他通用方法 如使用防火墙,给数据库系统及时打补丁,使用专用的工具检测及查杀漏洞等。