基础知识
在学习本期内容之前,先介绍本期用到的基础知识。
表
- 行:又称为记录或数据
- 列:一般称为字段
常用语句
- select
用法:select 字段1,字段2,字段3…… from 表名 (where 筛选条件)
翻译一下就是如果满足某条件,从某表选择某字段展示出来(所有行) - limit
用法:加在上述语句后面,如limit 0,1。表示限制显示的行数
第一个参数表示起始行(0开始计数),第二个参数表示显示的行数。这一句就代表显示第0行。limit 2,3就是显示第2行到第4行 - order by
用法:order by 字段名或字段序号
比如一张表users有三个字段(列),分别是id,username,passwd,你可以用select username from users order by id或者 by 1来这些记录按照id排序。默认升序,要降序可以order by 1 desc。也就是加一个descending的缩写。 - union(重要)
用法:select 字段1,字段2,字段3 from 表1 union select 字段1,字段2,字段3 from 表2
用来将两条查询语句的查询结果拼接起来显示。
注意:两条语句查找的字段数要一致。可以想象两张表如果是有不同列数是不能拼接起来的。
- select
常用字符
- 逻辑运算符
and 与;or 或;not 非。 - 单行注释
#
或--
- 加号,连接(在URL中等同于空格)
+
- 通配符
- *通配符的作用是匹配所有结果集。
比如select * from users就是将users整张表展示出来 - %通配符的作用是替代0个或多个字符,用在like中
相当于正则表达式里的*。比如select username from users where username like “a%min”。则可能匹配到username为amin,admin,aemin,addddddmin……这样的记录(如果有) - _通配符的作用是替代1个字符,用在like中
比如select username from users where username like “a_min”。则可能匹配到admin,aemin,afmin……这样的记录(如果有)
- *通配符的作用是匹配所有结果集。
- 逻辑运算符
数据库指纹
所谓数据库指纹,就是说我们可以根据报错信息来判断数据库类型NO 报错 数据库类型 1 You have an error in your SQL syntax; check the manual that corresponds to your MySQL
server version for the right syntax to use near ”1” LIMIT 0,1′ at line 1MySQL 2 ORA-00933: SQL command not properly ended Oracle 3 Microsoft SQL Native Client error ‘80040e14’ Unclosed quotation mark after the character string MSSQL GET和POST传参
我们在上网的过程中,需要和服务器进行交互。比如我们在搜索引擎搜索一个词语,就是向服务器传递了一个参数。服务器通过这个参数从数据库中找到我们想要的内容,再展示给我们。这就是传参的基本原理了。GET传参
如果你比较细心,你或许会发现你在浏览网站时有的时候浏览器地址栏你的URL是以?xx=xx这样的方式结尾的,这其实就是GET传参。GET方式传参的最大特点就是请求传递的参数最终都会显式地拼接到URL后面。POST传参
这种方式的传参不会显示在地址栏中,相对来说比较安全。输入密码这类敏感信息时通常会使用POST传参。
想象我的博客有这样一个界面:
1
2username:
password:需要你输入用户名和密码,你输入了zhangsan和123。如果我安全意识很低,做网站的时候给这个表单method属性设置为GET,那么你提交的时候地址栏可能就变成了https://bowenyoung.cf/?username=zhangsan&password=123。如果我比较贴心,我就会设置成POST,那么你还是把参数传给了我这个网站的服务器,但是地址栏上就看不到了~~你旁边的人也就没这么容易看到你的密码了~~。
网页URL编码
URL 只能使用 ASCII 字符集通过因特网进行发送。
由于 URL 通常包含 ASCII 集之外的字符,因此必须使用URL编码将 URL 转换为有效的 ASCII 格式。
URL编码也被用来防止SQL注入。知己知彼,百战不殆😜
可以参考W3School的这篇文章,还有编码转换工具:链接
注入原理
Q:数据库后端如何执行查询操作?
想象我的博客有一个登录界面需要输入用户名和密码,当你输入用户名和密码后,后端就会产生 并执行一条查询(SQL查询),登录成功后该查询结果会被显示在我们的主页。例如:1
2username: zhangsan
password: 123那么后端查询语句可能是这样:
SELECT * FROM 某张表 WHERE username='zhangsan' AND password='123';
如果用户名和密码都正确,就会返回这些内容。(当然会经过处理,一般不是直接看一张表)
当然,查询语句也可能是SELECT * FROM 某张表 WHERE username="zhangsan" AND password="123";
或者是SELECT * FROM 某张表 WHERE username=('zhangsan')AND password= ('123');
或者SELECT * FROM 某张表 WHERE username=("zhangsan")AND password="123";
或是其他形式,这取决于开发人员如何将参数封装起来。如果参数用单引号封装,输入
zhangsan'
会发生什么?
后端查询语句就变成:SELECT * FROM 某张表 WHERE username='zhangsan'' AND password='123';
显然'zhangsan''
这里多了一个单引号,发生了错误。在前端有报错回显的时候,这条语句的报错就直接显示在我们的浏览器上了。除了去掉这个多余的单引号,还有什么方法能不报错呢?
这时候我们就要用到之前讲的注释了。
假如我们输入zhangsan'--+
或者zhangsan'#
,后端查询语句就变成了:SELECT * FROM 某张表 WHERE username='zhangsan' --+' AND password='123';
或者SELECT * FROM 某张表 WHERE username='zhangsan' #' AND password='123';
我们知道,被注释掉的内容是不会被执行的,因此上述语句如果忽略注释的内容就成了:SELECT * FROM 某张表 WHERE username='zhangsan'
单引号就闭合了!这条语句是完全正确的!上面这条语句只有用户名也能查询吗?
如果开发人员没有进行过滤筛查,那么答案是肯定的!也就是说我们通过注释的方式,修改了SQL语句,使得我们用非正常的方式查询到了数据,这其实就是SQL注入的原理了。
开始实践
准备工作
我们打开PHPstudy面板将服务开启
在浏览器中输入localhost/sqli/Less-1
,注意将sqli改成你设置的文件夹名称。这样我们就来到了第一课。
根据提示”请输入ID作为数值参数“,加上这关是GET方式传参(关卡的信息可以看到)。所以我们在地址栏URL末尾加上?id=1,意思是通过GET方式传递一个数值为1的参数。回车,观察页面显示:
可以看到页面显示了登录名和密码。接下来我们增大id值2,3,4……一直到14,发现都显示了登录名和密码,但大于14之后不再显示。这是怎么回事?我们联系后端查询语句可以知道,这说明这张表有14个id对应了14条不同的记录(行)。我们已经获得了这张表的一些信息。
破坏查询
接下来我们要破坏查询。记得之前说的在值后面加一个单引号会发生什么吗?
是的,会报错。
我们尝试输入将1改成1’。页面显示如下:
当然这里单引号被转换为了URL编码,可以参考之前在基础知识里提到的文章。
现在我们分析这条报错语句,提示的是near ''1'' LIMIT 0,1' at line 1
这里有一个语法错误。这里的语句内容使用单引号包裹起来的,因此我们先去除最外层的单引号,可以知道后端查询语句这部分为:'1'' LIMIT 0,1
而我们输入的是1’,把这个值也去掉就会发现,我们输入的参数是被单引号包裹起来的。这样我们就判断出了注入的类型按参数分是单引号型。
如果你觉得使用单引号来判断注入类型看得不清楚,可以使用转义字符\
来判断。
将参数改为1\
。页面显示如下:
这样,转义字符\
后的字符就是将参数封装起来的字符。
值得一提的是,并非所有参数都由单引号封装。我们可以尝试用同样的方法去破坏LESS2,3,4,5,6的查询。结果分别如下图:
通过分析报错语句我们可以知道2中的参数是裸露的,没有用任何字符封装,3用')
封装,4用")
封装,5用'
封装,6用"
封装。
消除语法错误
知道了注入的参数是被什么字符封装起来的以后,我们就要考虑去除语法错误。
在前面我们已经讲过如何去除语法错误了。我们回到LESS1进行实验。
在地址栏URL末尾输入?id=1--+
或者?id=1'--%20
(%20 URL编码为空格)
或者?id=1'%23
(%23 URL编码为 # )
输入?id=1
时后端查询语句我们可以通过报错信息猜测后端查询语句为:
1 | SELECT * from table_name WHERE id='1' LIMIT 0,1 |
那么现在变成了
1 | SELECT * from table_name WHERE id='1'-- ' LIMIT 0,1 |
忽略注释掉的内容就是:
1 | SELECT * from table_name WHERE id='1' |
这时候单引号被我们闭合了,后面的语句被我们注释掉了,当然仍然能够成功执行!
同理,LESS2-6也可以根据参数被封装的类型,使用注释去除语法错误。
添加语句
我们通过注释的方法减少了原来语句的内容,那我们能否增加我们想要查询的内容呢?当然可以!
SELECT * from table_name WHERE id='1'【插入语句】-- ' LIMIT 0,1
我们在注释前插入语句,就可以增加查询的内容。如何实现呢?
我们会想到,在这里再加一条SELECT语句不就完事儿了吗?是的,但我们需要用到之前划重点的union。这里就涉及到一个问题,union连接的select语句必须查询字段数相同,但是我们并不知道原来的SELECT查询了多少个字段。我们先要知道查询结果有多少个字段(不一定所有查询结果都会显示再页面上,所以从页面上得不到信息)。
这时候就要用到order by了。我们在插入语句的地方加上order by 1
,如下图:
order by 1
表示按照查询结果的第一列进行排序。页面显示正确,说明查询结果有第一列(废话)。我们再增大列数,一直到order by 4
,出现了报错:
提示没有第4列,因此我们得到查询结果有且仅有3列。这时候就可以用union了。
新的问题来了,我们查询了3列,但每次改变id只改变了login name和password后面的内容,这不是两列吗?
的确是两列,因为经过开发人员处理,回显给我们的是3列中的两列。因此我们要判断显示的是哪两列。
这里先讲一下字面量的概念。我们之前说select后面加的是字段名,这时候会查询相应的列。但如果我们select 1,2,3呢?这时候并不是查询第1,2,3列,而是直接查询出来这几个数字1,2,3。这些数字就叫做字面量。利用查询字面量的方式我们可以判断回显了第几列。
试想一下,select 1,2,3这个语句被执行。页面上如果显示:
1 | Your Login name:1 |
这不就代表页面回显了查询结果的第1,2列吗?
因此我们会想到在插入语句处加上union select 1,2,3
。但是发现页面回显没有变化。这是因为总共只显示了id=1对应的两条记录。union后面的语句查询到了也没地方显示了。
只要让union之前的语句什么都查不到,就可以显示union后面的语句了!
也就是让where后面的条件对应不上表里的内容,我们可以将id改成0(或者负数,比记录数14还大的数),毕竟很有可能没有id=0的记录。也就是url末尾改成?id=0' union select 1,2,3--+
,如图:
发现页面回显2,3。说明页面回显了查询结果的第2,3列。这时候我们就可以把我们想要查询的数据放在第2,3列的位置,页面就会回显出来给我们。
通常来说,我们可能会想要获取的基本信息有:数据库类型、版本号,当前数据库名称,当前执行查询的用户名……通常都有相应的函数或常量来查询这些基本信息。下表给出了部分函数或常量:
函数或常量名 | 返回值 |
---|---|
system_user() | 系统用户名 |
user() | 用户名 |
current_user() | 当前用户名 |
session_user() | 链接数据库的用户名 |
database() | 当前使用的数据库名 |
version() | 数据库版本 |
@@datadir | 数据库目录 |
@@tmpdir | 操作系统缓存的临时目录 |
这里以获取数据库版本和当前数据库名为例
将1,2,3
改为1,version(),database()
,得到如下结果:
之前讲了数据库指纹,通过之前的报错信息我们可以知道使用的是MySQL数据库,结合此处5.7.26可知使用的是MySQL5.7.26版本,当前使用的数据库是security。
从PHPStudy面板来看,的确如此。
获取敏感信息
但光知道这点信息我们肯定不满足啊,我们肯定想知道当前数据库里有什么敏感信息,比如用户名密码以及用户的其他信息。
这时候要用到之前提到过的information_schema数据库以及其中的tables,columns表。
先来解释一下这些内容。
- information_schema数据库是MySQL自带的数据库。它提供了访问数据库元数据的方式。元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。
- 其中的tables表存储了所有的表名(table_name),以及表所在的数据库名(table_schema)等信息
- 其中的columns表存储了所有的字段名(column_name),以及字段所在的表名(table_name)等信息
接下来开始实验:
输入?id=0' union select 1,table_name,3 from information_schema.tables where table_schema=database() --+
。意思是从information_schema数据库中的tables表查询表名,条件是表所在的数据库是当前数据库。页面显示如下:
显示了emails,但数据库不太可能只有这一张表。这是因为开发人员让每一列只显示了一条记录。我们可以在语句后加上LIMIT 数字,1来限制显示第几行。比如加上LIMIT 0,1表示显示第0行,LIMIT 1,1表示显示第1行,第一行显示如下(?id=0' union select 1,table_name,3 from information_schema.tables where table_schema=database() limit 1,1--+
):
我们当然可以一次次改变limit的第一个参数来获取数据,但还有一种方法让所有记录一起显示出来。就是使用group_concat()这个函数。
例如,?id=0' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() --+
,页面显示如下:
这时候我们发现,所有的表就被列举出来了。我们可以看到这里存储的其实都是一些敏感信息,我们以users表为例去查看里面有哪些字段名。
输入?id=0' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+
。意思是从information_schema数据库中的columns表查询存储的字段名数据,条件是该记录的table_name表名是users。显示结果如下:
东西很多,我们就看看用户名和密码吧
输入?id=0' union select 1,group_concat(username),group_concat(password) from users--+
。意思是从当前数据库(security)中的users表查询其中的username列和password列。显示如下:
可以看到用户名和密码就一一对应地显示出来了!我们的目标基本就达到了!
当然这仅仅是以LESS1为例查询了部分信息,有兴趣的朋友可以尝试2-6关、查询其他的一些数据(因为是演示项目有些表里面可能没有数据)
结语
至此,我们对于基于报错的注入的学习就结束了。原理非常简单,但是需要练习来消化一下。
下一期的内容我们讲解布尔盲注,也就是LESS8。LESS7是将查询的内容输出为文件,不涉及注入的原理,我们就跳过了。
可以通过RSS订阅本篇博客来更早获知更新信息。也欢迎在博文下方留言或者邮箱联系我!
条评论