SQL注入详解
SQL注入指通过向服务器发送恶意的SQL语句或片段注入到服务器中,改变原有的SQL逻辑,从而实现攻击者的目的
- 比如在源代码中存在这样的语句:
public Object sqlInjection(String param){ ... String sql = "select * from user where username='" + param + "'"; resultSet = prepareStatement.executeQuery(sql); ... }
在变量
sql
定义中,param
参数通过拼接的方式称为了sql
语句的一部分。比如传来正常的参数admin,此时的sql语句便是select * from user where username='admin'
,该语句便会正常执行,并返回想要查询的结果。但是如果传来的值为' or 1=1#
,那么此时sql会变成select * from user where username='' or 1=1#'
。由于传来值的第一个'
号和前边的'
进行了闭合,因此后边的部分代码便变成了sql语句的一部分而不是一个值。此时由于or 1=1
永远成立,并且#
符将后边的'
注释掉,因此现在的sql语句变成了一段恶意代码,这便是sql注入
防护
sql注入的防护有两种方法,第一是对参数进行预编译,第二是在拼接参数前对参数进行安全校验,通过后才可以拼接到sql语句中。
预编译
当一条SQL语句执行前,需要先进行语法分析。如果没有使用预编译的方法来执行SQL,那么每次在执行SQL时都需要先进行语法分析,然后再执行。此时进行SQL注入,恶意的代码(payload)拼接上SQL语句之后才会进行语法分析,其中payload中包含的关键字等内容便会被识别,因此会被成功执行。
而预编译则是先在待传入参数的位置增加占位符(?
),并对SQL语句进行语法分析和编译,并将结果存入内存中等待调用。在调用时,传入的参数会自动放入占位符的位置,并执行SQL语句。此时如果进行注入,由于并没有重新进行语法分析,payload中包含的关键字等不会被识别,而是作为字符串的形式存在于语句中,因此payload不会成为SQL语句的一部分。
select * from user where username=?
,如果payload是' or 1=1#
时,执行的代码是:select * from user where username="' or 1=1#"
(为了阅读更加清晰,此处将''
替换为了""
),注入失败
参数校验
在一些情况下是不能使用预编译的方法来避免SQL注入(比如在某些业务场景下需要拼接字段名
String sql = "select * from user where " + username + "=?"
此时需要防止SQL注入,便需要对username进行校验:
-
类型转换:如果拼接上的值是整数或浮点数等,那么便可以对参数进行类型转换来避免SQL注入
String page = request.getParameter("page"); String size = request.getParameter("size"); String sql = "select * from user limit " + page + "," + size;
此时可以在拼接前先把
page
和size
两个参数转换成整型再进行拼接,如果两个参数中包含有恶意代码,则在类型转换时便会报错,从而避免SQL注入 -
白名单校验:设定一个白名单,当变量的值符合白名单中的某一个时,便可拼接
String username = request.getParameter("username"); //白名单校验 String whitelist = "username,password,gender"; String sql = ""; if(username != null && whitelist.indexOf(username) > -1){ sql = "select * from user where " + username + "=?"; }
注入方法
通常在有输入的地方,可以判断是否会执行SQL语句,比如查询等。当判断存在有SQL执行时,便可通过尝试注入。如果能够看到源代码,便通过分析源代码构建payload(其中分为字符型注入,数值型注入等情况);如果看不到源代码,则需要进行盲注(分为报错型盲注,时间型盲注和布尔型盲注)
数值型
(此处以靶场Pikachu为例)
-
分析代码,可得此处存在数字型注入
-
由于在放入个人的payload前要将前边的代码闭合(即让前边的条件成立),因此需要先随便放上一个数字
payload=3
,然后再跟上自己的代码:payload=2 or 1=1#
-
注入成功
字符型注入
查看代码
-
由于拼入的参数值是字符类型的,它被
''
所包裹,因此需要考虑怎样将''
闭合起来。构建payload='=0#'
-
注入成功
联合查询注入
-
联合查询注入是在判断此处存在sql注入后,通过使用
union all
来查看自己想要的数据 -
由于联合查询要求多个查询之间的字段数目相等,因此先使用正常的字符来判断它的返回结果有几个字段
-
可以判断可能存在三个字段,因此构建
payload=' union all select 1,2,3#
来进行尝试(最后一个3前加引号是为了和原sql中最后一个'
进行闭合) -
没有报错并且显示出了联合查询的内容,因此字段数目对应上,并且确定了每个字段的显示位置,因此便可构建新的
payload=' union all select database(),version(),3#
来查看自己想看到的内容 -
如果在上一步中字段数目不对,则可动态的添加或删除字段数来进行调试payload
盲注
盲注没有返回值或返回值不清晰,无法判断是否注入成功,因此需要想办法验证注入结果
boolean型
boolean型即根据条件的不同,得到不同的结果来判断是否存在SQL注入
-
当输入
admin
时,结果可以正常显示 -
构建payload
admin' and 1=1#
-
payload:
admin' and 1=2#
因此可以判断存在SQL注入
时间注入
使用
sleep()
函数可以使当前执行SQL的线程睡眠指定的时间,因此通过时间的长短来判断是否存在SQL注入
-
发送请求后,使用burpsuite抓包,并将参数的值改为payload后发包。(此处使用if函数为了让结果更直观明显)
-
对比结果
-
根据结果的不同可以判断,
sleep
函数成功执行,因此此处存在SQL注入
报错注入
在一些更新或者插入的地方,无法使用正常的那些注入方式,便可通过构建一个错误的语句,让系统报错,从而判断是否存在SQL注入
-
构建payload:
' or updatexml(1,concat(0x7e,database()),3,) or '
-
在报错中显示出了数据库名,因此此处存在报错注入,并且能导致信息泄露
扫描工具
判断是否存在SQL注入的神器 -> sqlmap
SQLMap
-
在kali系统中,默认安装好了sqlmap工具直接在控制台使用即可
-
使用sqlmap扫描接口。在控制台输入:
sqlmap -u "http://localhost/pikachu/vul/sqli/sqli_str.php?name=asd&submit=%E6%9F%A5%E8%AF%A2" -p name
回车后可以看到执行的过程和结果
-
sqlmap具体使用教程不过多介绍,深入了解请移步:sqlmap教程