SQL 注入
简单数据库操作
select
1 |
|
select + 列名(* 代表所有) from + 查询表 + where + 查询内容
上面查询语句等价于
1 |
|
从user
表,擦寻所有包含id
为1
的数据
同时我们可以使用()
提高对应的查询优先级,数据库将会先查询括号内内容,之后再根据括号外的内容进行查询
union
1 |
|
union
会同时查询两个项目,user
将会先被查询,随后执行查询emails
union 查询有一个限制,前后查询的列数必须要相同
group by
1 |
|
可以通过group by
来进行判断对应表的列数
order by
1 |
|
desc
是排列顺序变为降序
limit
1 |
|
限制为从第一行开始显示三行,数据库中表的行数从 0 开始计数
and 和 or
1 |
|
and
需要同时满足查询的前后两者关系,or
只需要满足其一即可进行查询
group_concat
1 |
|
将我们的查询内容归到一行中
select database()
用于查询当前数据库
select version()
用于查询当前数据库版本
SQL注入基础
注入分类
数字型 —— 当输入的参数为整形时,可以认为是数字型注入
字符型 —— 当输入的参数为字符串时,可以认为是字符型注入
注入点
注入点就是可以实行注入的地方,通常是一个访问数据库的链接
如何判断是字符型注入还是数字型注入
使用and 1=1
和 and 1=2
来判断
当是数字型注入时只有第一个能被执行第二个无法被数据库查询到,网页无法正常显示
当是字符型注入时两个都可以进行正常显示界面
字符型需要闭合符
1
$sql = 'select * from users where id ='$id' limit 0,1';
提交
1 and 1=1
时
1
$sql = 'select * from users where id ='1 and 1=1' limit 0,1';
单引号闭合语句后
Where
语句为一个条件id = '1 and 1=1'
数字型则不需要闭合符来闭合
1
$sql = 'select * from users where id = $id limit 0,1';
提交
and 1=2
时,将and
解析为了一个命令
1
$sql = 'select * from users where id = $id and 1=2 limit 0,1';
导致后面的查询出现错误,而不能正常返回
闭合方式
'
、"
、')
、")
、其他
判断闭合方式
我们可以通过制造报错来进行判断,在测试点输入数据后加上对应的闭合方式进行测试,通过网页的回显来进行判断闭合方式,当程序没有回显时,我们也可以采用加上 --+
或#
或%23
注释符来根据页面的正常显示与否来进行判断
闭合的作用
我们手工提交闭合符号,结束前一段的查询,后面即可加入其他语句,来进行查询我们需要的参数
union 联合注入前基础工作
我们分析出了对应的闭合方式后我们可以使用group by
来判断前面查询语句的列数
1 |
|
改变10
,多次测试后我们可以得到对应查询的列数,随后我们需要查询回显位,假设我们的列数为3
,那么我们使用1,2,3
进行占位,来判断回显位
1 |
|
一般来说页面只会读取第一行,因此我们id
前面的查询数据需要设置一个数,让前面的查询语句没有对应的匹配值,从而只显示我们的后面union
对应的查询语句
1 |
|
此时我们执行完后,观察页面的显示,看看是那一部分显示出来的,那么我们便可以更改对应的回显位,来完成我们需要的查找
1 |
|
Union注入
最终目标
使用union
注入拿到数据库中所有的用户名和密码
拿到表名
information_schema
包含了所有mysql
数据库的简要信息,其中包含有两个我们需要的数据表tables
(表名集合表)和columns
(列名集合表)
我们需要的表名信息在数据库information_schema
->数据表tables
->数据列table_name
1 |
|
过滤在security
数据库中的表名,即table_schema
为security
的行
1 |
|
因为查询的信息只能显示一个,因此我们需要使用group_concat()
确保所有查询到的信息能放到一行显示出来
1 |
|
拿到列名
用相似的方式我们进行获取数据库information_schema
->数据表columns
->数据列column_name
1 |
|
通过拿到列名后根据我们已有的信息可以获取到对应的数据库里面的所有用户名与密码信息,我们可以直接进行输出
1 |
|
大致总结流程如下:
- 确定是数字型还是字符型
- 使用
group by
进行二分判断union
语句中前一个查询的列数 - 优化语句,将对应查询的内容修改为不存在的内容
- 使用
select
,查询数据库库名 - 使用
select
,查询所有表名 - 使用
select
,查询所有列名 - 查询所有用户名密码
报错注入
报错注入是一种页面响应形式,主要在于后台对于输入输出的合理性没有检查
对于报错注入,我们需要构造语句让错误信息中夹杂可以显示数据库内容的查询语句,进而让服务器返回报错提示中包含数据库的内容
常用的方式有floor()
、extractvalue
、updatexml()
extractvalue() 报错注入
extractvalue()
通常用于查询对应的xml
文件信息的
1 |
|
对于此种方式进行注入时,我们需要关心的是第二个参数,第二个参数当把查询路径写错时不会参数对应的错误信息,但是当我们把路径的'/'
改为'~'
时便会产生对应的错误信息,但是后续的内容可以被数据库解析,从而查询我们所需要的内容
1 |
|
通过这样的方式来拿到我们的查询数据,需要注意的是这样的方式我们仅能返回32
个字符长度,为了解决这个方式我们需要使用到一个函数substring()
substring()
有三个参数:
第一个参数是对应控制的输出内容,第二个参数是输出的起始下标(注意此处下标起始为 1),第三个参数是对应的输出长度
1 |
|
updatexml() 报错注入
updatexml()
包含三个参数,第一个参数是对应的xml
文档名称,第二个参数是xml
文档的路径,第三个参数是替换查找到的符合条件的数据
1 |
|
updatexml()
的注入方式与extractvalue()
相同,都是通过修改对应路径使其发生报错,从而将我们路径后半部分的sql
语句查询到的数据进行显示
这两函数均有一个特性,仅能返回32
个字符长度,我们同样可以使用函数substring()
来进行解决这个问题
floor() 报错注入
相关涉及到的函数
rand()
函数:随机返回0 ~ 1
间的小数
floor()
函数:小数向下取整数,向上取整数ceiling()
concat_ws()
函数:将括号内数据用第一个字段链接起来
concat_ws(‘-’,a,b); -> a-b
as
:用于取别名
count()
函数:汇总统计数量
limit
:用于显示指定行数
构造方式
1 |
|
拆解构造
1 |
|
上面的命令将我们的database()
与floor(rand()*2)
用-
进行拼接,因为rand()
产生的数字在0 ~ 1
间,乘以 2 后返回的数字在0 ~ 2
间,我们对其进行向下取整,那么返回值就只有 0 和 1 ,我们此处拼接后的数据类型就只有两种,假设我们情况查询到的database()
名为user
,那么此处返回的两种类型分别为user-0
和user-1
随后我们将我们查询到的数据命名为a
,随后对a
进行分组(得到的数据为两种类型user-0
和user-1
)
随后我们使用count()
对我们分类后的数据进行统计数目
当我们执行此条语句时,偶尔会出现报错#1062
报错原因
当我们给rand()
函数一个固定的seed
那么其产生的随机数经过floor(rand()*2))
处理后变得是一个固定值,一般我们给定为0
我们可以把count(*)
去除,可以发现不会再报错,说明是在统计的时候产生了错误,那么为什么会产生错误信息呢
rand()
函数进行分组group by
和统计count()
时可能会多次执行,导致键值key
重复
人话解释:
我们把
floor(rand(0)*2))
设置后产生的数列为0、1、1、0、1…当开始统计时,
group_key
[理解为分类的名单]中没有任何的分类方式,我们第一次统计0
的时候发现这一点后,group_key
需要重新计算并把结果写入键值[相当于此次计算时在创建group_key
的一个分类],此时执行的时第二次计算,而将1
写入了group_key
中,而第二次统计时group_key
以及存在对应键值了,可以直接写入统计数目,当再次写入到0
时,需要再次计算,而此时计算的结果是1
,但是1
的分类已经被创建了,无法再次创建,进而引发报错
一些情况
有些时候我们注入后会发现原本会报错的注入语句突然正常执行了起来,此时我们可以试试看将group_concat()
换成concat()
或者concat_ws()
,同时也可以加上一个limit
将对应的回显行进行修改
布尔盲注
盲注:页面没有报错回显,不知道数据库具体返回值的情况下,对数据库中的内容进行拆解,实行SQL
注入
布尔盲注:页面就只有两种状态,一种是为真,一种为假
相关函数
ascii()
:获取对应字符的ASCII
码
substr()
:有三个参数,第一个为一个长字符串,第二个为输出字符串的起始位置(从1
开始计数),第三个为控制显示字符的个数
相关注入方式
布尔盲注的方式主要是通过不断地尝试来一个个符号的进行爆破,当输入正确的时候网页会回显对应正确信息,通过这个方式进行逐项爆破
1 |
|
我们通过不断的修改xxx
的值,直到找到一个值使语句恒能使页面正常显示,此时我们便找到了一个字符,多次进行下去便可以获取到我们想要的信息
时间盲注
web
页面只返回一个正常页面,利用页面响应时间不同,逐个拆解数据
前提是数据库会执行命令代码,只是不反馈页面信息
相关函数
sleep()
:参数仅有一个,对应为休眠时长,以秒为单位,可以为小数
if(a,b,c)
:判断a
的真假,当a
为真时执行b
,否则执行c
相关判断
我们通过页面的返回时间来进行对应的语句判断,我们可以通过网络
这个调试组件来进行观察网页的返回时间,判断是否延迟了我们输入的秒数
注入语句
1 |
|
相当于我们利用ascii
和substr
的组合将第查询到的数据的第一个字母进行返回,同时通过if
进行判断,通过这个方式我们可以根据延迟的长短来判断出我们输入的语句是否正确,通过这样的一个个数据的判断,我们可以拿到对应的内容
SQL 注入文件上传
相关要点
1 |
|
查看MySQL
是否有读写文件权限
数据库的file
权限规定了数据库用户是否有权限,向操作系统内写入和读取已存在的权限
into outfile
命令使用的环境:必须知道一个服务器上可以写入文件的文件夹的完整路径
文件上传指令
1 |
|
简单的一句话木马
对目标网站上传指令
1 |
|
XXX
为文件路径,123.php
是我们插入的文件名
文件上传后
我们执行完上面的语句后会在对应的路径生成一个123.php
文件,这个就是我们的后门了,我们通过蚁剑进行链接即可达到控制整个服务器的效果
DNSlog 手动注入
load_file()
1 |
|
XXX
为对应文件的路径,此条命令可以去读取对应文件内容,同时显示对应字节
需要用到的网站
1 |
|
注入原理
相当于我们加载文件中的一个路径中如果有sql
语句则被数据库进行解析后,我们可以通过查看访问对应的DNSlog
进行查看,在DNSlog
中会显示访问的网站,其中便有已经解析完毕的sql
语句,通过这种方式进行查找,便可以获得对应数据库的信息
load_file() 读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有 FILE 权限。 该文件所有字节可读,但文件内容必须小于 max_allowed_packet,这个函数也可以用来发送 dns 解析请求,并且只能在 Windows 平台发起 load_file 请求
注入过程
我们可以先在上面两个网站中获取到一个随机的域名,这个域名钱可以加任何内容,对应的访问都会被记录下来
1 |
|
比如我们访问
1 |
|
那么对应的网站便会将其进行记录,利用这个特点我们可以进行手动构造注入语句,完成相关的注入过程
1 |
|
此时我们回到获取域名的网站观察访问的网站域名
便可以看到对应的数据库名了,我们只需要对select database()
进行修改即可,修改成为我们对应需要的查询语句
DNSlog 自动化注入
相关工具
1 |
|
相关使用
1 |
|
Post Union注入
Get提交与Post提交的区别
get
提交可以被缓存,post
提交不会被缓存
get
提交参数会保留在浏览器的历史记录里,post
提交不会
get
提交可以被收藏为书签,post
踢提交不可以
get
提交有长度限制,最长2048
个字符,post
提交没有长度要求,不是只允许使用ASCII
字符,还可以使用二进制数据
注入方式
post
注入的方式与get
注入方式相同,只不过需要使用post
进行发送我们的注入语句
一般post
注入前,我们可以通过抓包获取到post
发送的消息,将其发送的消息中插入我们的攻击语句即可
Post盲注
post
盲注方式和get
盲注相同,同样post
盲注的特点是需要使用post
的方式进行发送请求包
与一般的union post
注入相同,我们需要先抓一下包,讲对应的请求信息进行构造,然后修改对应语句,插入我们攻击语句,达到攻击的目的
Http头UserAgent注入
页面看不到明显变化,找不到注入点,我们可以尝试报头注入
一般来说这种注入方式需要我们拥有对应的账户和密码,因为在此时我们登录上时会产生对应的UserAgent
信息,我们可以通过BurpSuit
抓包,然后修改对应的UserAgent
信息,在里面加入我们的攻击代码。
一般来说对于此种方式我们一般使用报错注入。
1 |
|
主要过程
开启代理,使用BurpSuit
进行拦截数据包,将数据包的UserAgent
信息进行修改,插入我们的注入代码,使用Post
将修改后的数据提交,获取反馈信息
Http头Referer注入
当我们访问一个页面时,会产生一个referer
信息,记录了我们是从哪一个网站访问到这个页面的
本来正确的单词应该是
referrer
,因为早期HTTP
规范的拼写错误,为了保持向后兼容就讲错就错了
与UserAgent
注入相同,我们同样是需要进行抓包来获取到对应的请求信息,将对应的referer
信息进行插入我们的注入语句,通常是使用报错注入的方式进行攻击
1 |
|
Http头Cookie注入
Cookie
相当于我们之前登录的一种凭证,在Cookie
有效期内,客户端只需要向服务器发送Cookie
进行验证,不需要再次输入用户名和密码
如果Cookie
(未加密)被放到服务器时数据库会进行解析,我们可以尝试进行注入
一般在Cookie
注入我们考虑顺序为:Union
注入->报错注入->布尔盲注->时间盲注
注释符绕过
注释符号作用
将后面不需要的语句注释掉,保证句子的完整性
常用注释符
--
、#
、%23
绕过
可以手动在添加一个闭合方式,比如:闭合方式为')
我们可以再添加一个('
,使其前后完成闭合
我们也可以使用等式
1 |
|
我们将后面的一个位置进行改为一个等式的判断,同时利用原来的方式进行闭合操作,那么我们遍可以绕过掉注释
and和or过滤
一般and
和or
的过滤方式在源代码中使用正则匹配来进行匹配,对于匹配到后的替换为空字符,相当于将我们的输入中的and
和or
进行删除
对于此种方式我们可以尝试使用以下方式绕过:
大小写绕过
例如:?id=1' anD 1=1 --+
复写过滤字符
例如:?id=1' anandd 1=1 --+
使用替代
使用&&
或者是||
来进行取代and
和or
例如:?id=1' && 1=1 --+
如果直接使用&&
或||
发生报错时,我们可以将其转换为url
编码进行输入
空格过滤
空格过滤在绕过防火墙有着比较实际的意义
一般我们可以使用如下方式进行绕过:
使用加号
如:?id=1'+and+1=1--+
使用URL编码
我们可以将空格转换为URL
编码%20
尝试进行绕过,如?id=1'%20and%201=1--%20
使用注释
我们也可以在中间可以插入/**/
,来进行替代空格,如:?id=1'/**/and/**/1=1--/**/
使用Shell变量
我们可以使用$IFS$1
来替代空格,因为$IFS
默认是空字符(空格Space
、Tab
、换行\n
)
使用报错注入
我们可以使用报错注入来进行绕过空格输入
1 |
|
使用括号
我们可以将空格替换为括号进行包裹来进行绕过
1 |
|
逗号过滤
我们可以使用join
进行绕过
join
用于把来自两个或多个表的行结合起来,基于这些表之间的共同字段
我们利用join
的特性,则有如下等价:
1 |
|
等价于
1 |
|
那么对于我们的查询我们可以修改括号内的数字,将其替换为我们的注入语句
1 |
|
因为逗号被过滤了,我们使用group_concat
中的逗号将会失效,我们只能对于一个个进行查找,如下示例:
1 |
|
上面的查询语句会被解析为usernamepassword
,因此我们就只能改为单个进行查询
1 |
|
Union和Select的绕过
我们一般先进行检测对应的闭合方式,随后检测空格等其他方式是否被过滤,之后测试select
和union
使用大小写
我们将union
中的大小写进行改变,如:unIoN
使用复写单词
我们通过将union
变形,在其中再插入一个union
,如:uniunionon
使用报错注入
1 |
|
使用url编码
我们可以将其中的一些字母转换为url
编码尝试进行绕过
宽字节注入绕过
函数addslashes()
函数在指定的预定义字符前添加反斜杠,这些字符是单引号(')、双引号(")、反斜线(\)、NULL字符
当存在这个过滤时,我们输入的?id=1' --+
会被转义为?id=1\' --+
那么数据库会去查找的是1'
这个东西
因此我们使用宽字节注入的目的就是绕过这个转义的过程
我们利用这种方式进行注入需要利用到GBK编码
的特性,我们在单引号前加入一个%df
形成%df'
为什么这个方式就可以进行绕过呢?我们根据addslashes()
函数的一个特性会在单引号前插入反斜线,在GBK编码
中 \ 的编码位为%5c
,则构成了一个%df%5c'
符合了GBK编码
的取值范围(第一个字节129-254,第二个字节64-254),则其会解析为一个汉字,然后由addslashes()
函数所添加的 \ 便会失去应有的作用
一些其他绕过方式
注释
存在过滤时,如union select
被防火墙过滤时,我们可以在中间插入注释来尝试混淆绕过
1 |
|
或者是被过滤时我们可以加上!
让我们的注释会被执行
1 |
|
或者我们还可以加上一串数字如:50000
1 |
|
这个表示数据库是5.00.00
以上版本时,该语句才会被执行后面的abc
我们还可以使用注释--+
1 |
|
空白符
使用url
编码尝试绕过
%09
:水平制表
%0a
:新一行,MySQL
可以让命令分行输入
%0c
:新一页
%0b
:纵向的 TabLayout
替换
当information_schema.tables
被过滤时,我们可以使用另外的两张表
1 |
|
当information_schema.columns
被过滤时,我们同样可以使用替换,我们采用join
获取列名
1 |
|
超大数据包
仅能用于Post
提交,对于安全狗3.5
版本时,出现被阻拦时会出现'qt-block-indent:0; text-indent'
的字样,对此我们可以使用Python
的requests
库来发送请求包,在我们的注入语句中加入/*!XXX*/
更改XXX
的长度,来尝试进行传大数据包绕过对应的防火墙
分块传输绕过
同样仅适用于Post
提交方式,分块出啊u你是将我目的数据拆分成多个部分,然后由服务器端重新组合,然后完成对应的绕过
需要添加一个分块传输的头
1 |
|
随后使用BurpSuit
的相关插件即可完成对应的传输
堆叠注入
当我们存在有select
过滤时,我们大部分的注入方式都失效了,我们此时可以考虑堆叠注入的方式来进行
我们使用show
来查看表名和列名
1 |
|
注意,如果tableName
是纯数字,需要用 ` 包裹,比如 1’;desc `1919810931114514`;#
当我们找到对应的flag
位置后,可以采用预编译的方式拼接select
进而绕过对其的过滤
1 |
|
或者将select * from `1919810931114514` 替换为其16进制的形式
1 |
|
预编译
预编译相当于定一个语句相同,参数不通的Mysql模板,我们可以通过预编译的方式,绕过特定的字符过滤
格式:
1
2
3PREPARE 名称 FROM Sql语句 ? ;
SET @x=xx;
EXECUTE 名称 USING @x;举例:查询ID为1的用户:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16方法一:
SElECT * FROM t_user WHERE USER_ID = 1
方法二:
PREPARE jia FROM 'SElECT * FROM t_user WHERE USER_ID = 1';
EXECUTE jia;
方法三:
PREPARE jia FROM 'SELECT * FROM t_user WHERE USER_ID = ?';
SET @ID = 1;
EXECUTE jia USING @ID;
方法四:
SET @SQL='SElECT * FROM t_user WHERE USER_ID = 1';
PREPARE jia FROM @SQL;
EXECUTE jia;
更改表名
- 修改表名:
ALTER TABLE 旧表名 RENAME TO 新表名;
- 修改字段:
ALTER TABLE 表名 CHANGE 旧字段名 新字段名 新数据类型;
handle
handle不是通用的SQL语句,是Mysql特有的,可以逐行浏览某个表中的数据,格式:
1 |
|
其他
万能符号
1 |
|
过滤 substr() 和 mid() 等
1 |
|
SQL_MOD
SQL_MOD
是MySQL
支持的基本语法、校验规则
假如我们有如下查询语句
1 |
|
当我们的输入在Flag
中查询到时,||
(或运算符)的作用下会只有非零数字和flag
才可以返回真值
对此我们可以改变||
在sql
中的语义,将其转换为连接符
其中
PIPES_AS_CONCAT
:会将||
认为字符串的连接符,而不是或运算符,这时||
符号就像concat
函数一样
我们使用堆叠注入
1 |
|
便可以对||
进行绕过