Web 安全防护指南基础篇-SQL注入

ʕ •ᴥ•ʔ (๑˃̵ᴗ˂̵) ପ( ˘ᵕ˘ ) ੭

1 SQL 注入

SQL 注人是指攻击者通过把恶意 SQL 命令插人到 Web 表单的输人域或页面请求的查询字符串中,并且插人的恶意 SQL 命令会导致原有 SQL 语句作用发生改变,从而达到欺骗服务器执行恶意的 SQL 命令的一种攻击方式。

如果数据库开启了写权限,攻击者可利用数据库的写功能及特定函数,实现木马自动部署、系统提权等后续攻击。

1.1 SQL注入攻击原理

SQL 注入产生的原因是用户提交参数的合法性。

SQL 注入的本质是恶意攻击者将 SQL 代码插入或添加到程序的参数中,而程序并没有对传入的参数进行正确处理,导致参数中的数据被当作代码执行,并最终将执行结果返回给攻击者。

SQL注人的产生原因通常有以下几点:

(1)参数处理问题:

  • 对用户参数进行了错误的类型处理。
  • 转义字符处理环节产生遗漏或可被绕过。

(2)服务配置问题:

  • 不安全的数据库配置。
  • Web应用对错误的处理方式不当。
    • 不当的类型处理。
    • 不安全的数据库配置。
    • 不合理的查询集处理。
    • 不当的错误处理。
    • 转义字符处理不当。
    • 多个提交处理不当。

1.2 SQL注入的分类

主要分为两类:

  • 回显注入
  • 盲注

在了解 SQL 注人的流程之前需要知道的是:在实际使用中,Web 服务器的配置情况会直接决定 SQL 注人的成功与否。在服务器配置及防护方面,能影响 SQL 注人过程的主要有以下几个方面:

1 )数据库是否开启报错请求。

2)服务器端是否允许数据库报错展示。

3)有过滤代码机制。

4)服务器开启了参数化查询或对查询过程预编译

5)服务器对查询进行了限速。

1.3 回显注入

SQL 注入漏洞的产生原因千差万别,主要与所用的数据库架构、版本有关系。

数据库可分为:

  • 关系型数据库:Oracle、MySQL、SQL Server、Access等。
  • 非关系型数据库:MongoDB等。

典型的攻击流程如下:

  1. 判断 Web 系统使用的脚本语言发现注人点,并确定是否存在 SQL 注人漏洞
  2. 判断 Web 系统的数据库类型
  3. 判断数据库中表及相应字段的结构
  4. 构造注入语句,得到表及相应字段的结构表中数据内容。
  5. 查找网站后台管理员,用得到的管理员账号和密码登录。
  6. 结合其他漏洞,上传Webshell并持续连接(留后门)。
  7. 进一步提权,得到服务器的系统权限

以上为标准的 SQL 注人流程,最终的效果是获取目标站点的系统控制权限。在实际安全防护中,由于应用系统的业务特点各不相同,导致在每个阶段可获取的内容并不相同。并且5、6、7步其实与 SQL 注入没有直接关系,但可归类为 SQL 注人后的延伸攻击手段。

SQL 注入攻击中常见的攻击工具有

  • 啊 D 注人工具
  • havji
  • SQLmap
  • pangolin

等,这些工具用法简单,能提供清晰的UI界面,并自带扫描功能,可自动寻找注人点、自动查表名、列名、字段名,并可直接注人,可查到数据库数据信息。其标准流程如下:

查找注人点 → 查库名 → 查表名 → 查字段名 → 查重点数据

1.3.1 寻找注入点

寻找注入点可以利用有回显的方法来确定页面是否有漏洞,如果没有回显,则可以进一步利用盲注的方法进行注入点探测。

盲注与回显注入基本相同,都是构造错误语句触发 Web 系统异常并观察

如果页面对查询内容有回显,则可以利用基于数字或布尔的方法来探测是否存在漏洞,加入构造如下三个链接:

下三个链接,并观察页面的特点。

  1. http://www.test.com/showdetail.php?id=49
  2. http://www.test.com/showdetail.php?id=49' and '1'='1
  3. http://www.test.com/showdetail.php?id=49' and '1'='2

访问以上三个链接时,产生的情况可能有如下几种:

  • 页面没有变化:访问三个链接,显示的页面没有任何不同。这种情况说明后台针对此查询点的过滤比较严格,是否存在 SQL 注人漏洞还需进行后续测试。
  • 页面中少了部分内容:如访问前两个链接正常,第三个页面里有明显的内容缺失,则基本可以确定有漏洞存在。接下来就需要检测是否有union显示位,如果没有,也可尝试进行bool注人(详情参见后续关于盲注的介绍)。
  • 错误回显:如果访问第三个链接后出现数据库报错信息,那么可以判定当前查询点存在注人,用标准的回显注人法即可实现 SQL 注人攻击。
  • 跳转到默认界面:如果第一个链接显示正常,第二、第三个链接直接跳转到首页或其他默认页面,那么这可能是后台有验证逻辑,或者是有在线防护系统或防护软件提供实时保护。之后可尝试绕过防护工具的思路(大小写混用、编码等)。
  • 直接关闭连接:如果在访问上述第二、三个链接时出现访问失败,那么这种情况下可尝试利用 Burpsuite 抓取服务器响应包,观察包头 server 字段内容。根据经验,这种情况通常为防护类工具直接开启在线阻断导致,后续可利用编码、换行等方式尝试绕过(极难成功)。

1.3.2 通过回显位确定字段数

回显位指的是数据库查询结果在前端界面中显示出来的位置,也就是查询结果返回的是数据库中的哪列。在 SQL 注人中,一般利用order byunion select等命令获取回显位的信息来猜测表内容,前者用来判断数据表的列数(字段数),后者用来判断回显位(字段)。具体使用方法如下:

XX.php?id =1' order by 4#

使用order by的主要目的是判断当前数据表的列数。在测试过程中可修改对应的数值。如果输人的数值大于当前数据表的列数,则查询语句执行失败,由于页面有报错信息,则可以由此判断该数据表的列数

XX.php?id =1' and '1'='2' union select 1,2,3#

使用union select来判断回显位,当得到回显位确定是哪一列或哪些列之后,则可以构造语句来获取数据表的内容。

union select中经常会用到NULL起到占位的作用,从而避免显示结果干扰判断。

1.3.3 注入并获取数据

尝试获取表、字段、数据的信息。

在 MySQL 5.0 之后的版本中,数据库内置了一个库information_ schema,用于存储当前数据库中的所有库名、表名等信息。因此,可利用 SQL 注人方式,通过远程注入查询语句方式实现直接读取 MySQL 数据库中的information_ schema库的信息,从而获取感兴趣的信息。SQL 注入在information_ schema库中主要涉及内容可参考下图。

information_ schema

在 SQL 注入过程中,可直接查询information_ schema库来获得目标信息。这里要注意的是,如果需要对表名进行爆破,那么表名需为十六进制格式

基本注入语句可参考以下格式(回显位是4):

  • union select 1,2,3,table_name,from(select * from information_schema.tables where table_schema=hex(数据库名) order by 1 limit 0,1)
  • union select 1,2,column_name,from(select* from information_schema.columns where table_name=hex(表名) and table_schema=hex(数据库) limit 0,1)
  • union select 1,2,字段 from 表名 limit 0,1

1.4 盲注

相对于普通注人来说,盲注的难点在于前台没有回显位,导致无法直接获取到有效信息。只能对注人语句执行的正确与否进行判断,也就是只有true 和 false的区别,因此盲注攻击的难度较大。在实施盲注时,关键在于合理地实现对目标数据的猜测,并利用时间延迟等手段实现猜测正确与否的证明。

常使用的函数有:

  • leng():获取数据库、表、字段长度等。
  • information_schema():获取数据库信息。
  • 定向获取字符
    • mid():获取字符串的值。
    • substring():获取字符串的值。
  • 时间延迟
    • benchmark(count,expr):重复计算expr表达式count次来达到延时作用,但是内存消耗很大,会导致系统无响应。
    • sleep(n):暂停数据库 n 秒

mid()substr()都是substring的同义词。语法是:mid(str,pos,len)str 是字符串,pos是起始子字符串的位置(第一个字母为1),len是一个可选参数,它决定从起始位置返回的字符个数。例:

SELECT MID('I drink coffee', 3, 5) Result; 结果为:drink

MySQL MID()函数的用法详解(代码示例)

MySQL substring()函数

MYSQL BENCHMARK()函数

Myql SLEEP函数和SQL注入

1.5 常见防护手段及绕过方法

SQL 注人的防护方法包括:

  • 参数过滤
  • 预编译处理

参数过滤分为

  • 数据类型限制(类型和长度)
  • 危险字符处理

通俗地说就是:要么严防死守,细致检查;要么严格限定参数的有效范围(参数化查询)。总之就是要尽可能限制用户可提交参数的类型。

针对 SQL 注入设计防护体系时,一定要与真实的业务场景进行配合,很多时候用简单的方式可获得非常好的防护效果。首先需尽可能详细地限制允许用户输人的参数类型及长度;其次,需考虑用户输人内容的特点及目的,开展有针对性的关键字、词过滤;如果是新建系统,推荐利用参数化查询手段,以实现更好的防护效果。当然,在这期间,应尽可能保证中间件版本的更新频率,可有效防护各类型攻击。以下将针对每种防护场景进行探讨。再次强调,防护力较弱的方法并不一定不适用,必须与实际环境相结合来选择。

1.5.1 参数类型检测及绕过

如链接:xxxx.php?type_id=4&new_id=690,其中type_id=4new_id=690为数字类型且长度均为一定值,均为用户可控,避免参数中出现非数字类型字符,并且对长度进行限制。或使用正则表达式可有效避免 SQL 注入。

防护方法

  • 使用函数对参数类型进行过滤
  • 正则表达式

参数类型检测主要面向字符型的参数查询功能,可以用以下函数实现:

  • int intval ( mixed $var [, int $base = 10 ] ) :通过使用指定的进制 base 转换(默认是十进制),返回变量varinteger 数值。
  • bool is_numeric ( mixed $var ) :检测变量是否为数字或数字字符串,但此函数允许输人为负数和小数
  • ctype_digit: 检测字符串中的字符是否都是数字,负数和小数无法通过检测

在特定情况下,使用这三个函数可限制用户输人数字型参数,这在一些仅允许用户参数为数字的情况下非常适用,如查询 ID 号、学号、电话号码等业务场景。

绕过方法

当 Web 应用对数据进行数字类型的限制时,受制于字符类型要求,因此无法构造出有效的语句,也就无法利用 SQL 注人攻击来获取数据库内的信息。

但能使用某些技巧令数据库报错,如is_numeric支持十六进制与十进制,提交0x01时它也会进行查询; intval虽然默认只支持十进制数字,但依然会有问题,比如提交id=-1时会出错。这些细微的差异可以帮助攻击者识别后台的过滤函数。

将字符串'1 or 1'转换为十六进制为0x2731206f72203127则可以进行绕过。

==绕过

PHP中有两种比较符号:

  • ===:会同时比较字符串的值和类型
  • ==: 会先将字符串换成相同类型,再作比较,属于弱类型比较

==对于所有0e开头的比较都判为相等, is_numeric() 判断变量是否为数字或数字字符串is_numeric()函数会判断如果是数字和数字字符串则返回 TRUE,否则返回 FALSE,且 PHP 中弱类型比较时,会使'1234a' == 1234为真,或者'12345%00',该函数还可能造成 SQL 注入,例如将‘1 or 1'转换为16进制形式,再传参,就可以造成 SQL 注入。

1.5.2 参数长度检测及绕过

防护思路

当攻击者构造 SQL 语句进行注入攻击时,其 SQL 注入语句一般会有一定长度, 并且成功执行的 SQL 注入语句的字符数量通常会非常多,远大于正常业务中有效参数的长度。

因此,如果某处提交的内容具有固定的长度(如密码、用户名、邮箱、手机号等),那么严格控制这些提交点的字符长度,大部分注入语句就没办法成功执行。

PHP 下可以使用函数strlen来统计出入参数的长度,然后可使用代码对长度进行限制。

绕过方法

可使用注释符、and、or 等修改原有语句意图。

如使用注释符:SELECT COUNT(*) FROM Login WHERE UserName='test'--' AND Password='(1)'";

由于注释符的存在,此语句实际执行的内容则变为:SELECT COUNT(*) FROM Login WHERE UserName='test' ;,只要有test用户存在,数据库就返回正确。

1.5.3 危险参数过滤及绕过

防护方法

常见的危险参数过滤方法包括关键字内置函数敏感字符的过滤,其过滤方法主要有如下三种:

  • 黑名单过滤

    将一些可能用于注人的敏感字符写人黑名单中,如' (单引号)、unionselect等,也可能使用正则表达式做过滤,但黑名单可能会有疏漏。

  • 白名单过滤

    例如,用数据库中的已知值校对,通常对参数结果进行合法性校验,符合白名单的数据方可显示。

  • 参数转义

    对变量默认进行adsalashes (在预定义字符前添加反斜杠),使得SOL注人语句构造失败。

  • GPC 过滤

    GPC 是GET、POST、COOKIE 三种数据接收方式的合称。在 PHP 中,如果利用$_REQUEST接受用户参数,那么这三种方式均可被接收。在早期PHP中,GPC过滤是内置的一种安全过滤函数,若用户提交的参数中存在敏感字符单引号'、双引号"、反斜 线\NULNULL字符),就在其前端添加反斜杠

由于白名单方式要求输出参数有着非常明显的特点,因此适用的业务场景非常有限。总体来说,防护手段仍建议以黑名单+参数转义方式为主,这也是目前针对 SQL 敏感参数处理的主要方式。

绕过方法

利用参数变化绕过黑名单防护,白名单没有很好的方法绕过,利用宽字节注入绕过 GPC 过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{% tabs 参数绕过方法 %}
<!-- tab 黑名单 -->

黑名单过滤一般试图阻止 **SQL 关键字**、特定的单个字符或空白符,那么绕过黑名单防护措施的核心思路就是:将关键字或特定符号进行不同形式的变换,从而实现绕过过滤器的目的。针对黑名单,目前存在的绕过方式有以下几种。

- 使用大小写变种

- 使用SQL注释代替空格(假设后台已经过滤空格,数据库会自动忽略注释符,执行时将其变成空格。

- 嵌套过滤

在过滤器阻止的字符前面增加一个采用 URL 编码的空字节`%00`。嵌套过滤后的表达式如`selecselectt`。过滤之后的部分就可重新结合成`select`。

- `+`号拆分字符(`or`等价于`'o'+'r'`)

- 利用系统注释符截断(`--`可对后续语句进行截断)

- 替换字符

- 用`like`替代`=`,可以绕过对`=`的过滤。
- 用`in`替代`=`,可以绕过对`=`的过滤。

<!-- endtab -->

<!-- tab GPC 过滤 -->

GPC 过滤是**PHP在5.4版本之前**存在的一种防护机制,其特点是在特殊字符前面加斜线`\`,如单引号`'`会形成`\'`的效果,导致原有的功能失效。因此,针对 GPC 过滤的情况,要对 GPC 添加的`\`进行转义。这种情况下可尝试{% label default @宽字节注人 %}方法。

{% note danger %}

宽字节带来的安全问题主要是编码转换引起的“吃 ASII 字符”(一字节)的现象,如果合理拼接,可让吃掉一字节后的剩余内容重新拼接成一个单引号`'` (举例)。

{% endnote %}

下面来分析 MySQL 的字符集转换过程。MySQL 收到请求时将请求数据从`charaeter_set_client`转换为`character_set_connection`。进行内部操作前将请求数据从`character_set_connection`转换为内部操作字符集,其确定方法如下:

1. 使用每个数据字段的`CHARACTER SET`设定值。
2. 若上述值不存在,则使用对应数据表的`DEFAULT CHARACTER SET`设定值(MySQL 扩展,非 SQL 标准)。
3. 若上述值不存在,则使用对应数据库的`DEFAULT CHARACTER SET`设定值。
4. 若上述值不存在,则使用`character set server`设定值。

最终将操作结果从内部操作字符集转换为`character_set_results`。

{% note warning %}

宽字节注人发生的原因就是 PHP 发送请求到 MySQL 时字符集使用`character_set_client`设置值进行了一次编码,如果编码为 GB2312、GBK、GB18030、 BIG5、Shift_ JIS 等双字节编码,就会存在宽字节注人漏洞。

{% endnote %}

<!-- endtab -->

<!-- tab 白名单 -->
白名单防护严格限制输出内容,因此没有很好的绕过手段。
<!-- endtab -->
{% endtabs %}

1.5.4 针对过滤的绕过方式汇总

(1)尖括号(大于小于)过滤绕过方式

用于替代大于>或小于<符号或等号=主要使用函数:betweengreatest

between mix and max

  • 可利用between 114 and 114来替代=114,也就是字符r
  • 可利用between 113 and 115来替代>113 and <115

greatest(a,b)

  • greatest(ascii(mid(user(),1,1)),140)>=140,判断user()第一个字符的ASCII

1.5.5 预编译后参数化查询

数据库编译原理和注入