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等。
典型的攻击流程如下:
- 判断 Web 系统使用的脚本语言,发现注人点,并确定是否存在 SQL 注人漏洞。
- 判断 Web 系统的数据库类型。
- 判断数据库中表及相应字段的结构。
- 构造注入语句,得到表及相应字段的结构表中数据内容。
- 查找网站后台管理员,用得到的管理员账号和密码登录。
- 结合其他漏洞,上传Webshell并持续连接(留后门)。
- 进一步提权,得到服务器的系统权限。
以上为标准的 SQL 注人流程,最终的效果是获取目标站点的系统控制权限。在实际安全防护中,由于应用系统的业务特点各不相同,导致在每个阶段可获取的内容并不相同。并且5、6、7步其实与 SQL 注入没有直接关系,但可归类为 SQL 注人后的延伸攻击手段。
SQL 注入攻击中常见的攻击工具有
- 啊 D 注人工具
- havji
- SQLmap
- pangolin
等,这些工具用法简单,能提供清晰的UI界面,并自带扫描功能,可自动寻找注人点、自动查表名、列名、字段名,并可直接注人,可查到数据库数据信息。其标准流程如下:
查找注人点 → 查库名 → 查表名 → 查字段名 → 查重点数据
1.3.1 寻找注入点
寻找注入点可以利用有回显的方法来确定页面是否有漏洞,如果没有回显,则可以进一步利用盲注的方法进行注入点探测。
盲注与回显注入基本相同,都是构造错误语句触发 Web 系统异常并观察。
如果页面对查询内容有回显,则可以利用基于数字或布尔的方法来探测是否存在漏洞,加入构造如下三个链接:
下三个链接,并观察页面的特点。
http://www.test.com/showdetail.php?id=49
http://www.test.com/showdetail.php?id=49' and '1'='1
http://www.test.com/showdetail.php?id=49' and '1'='2
访问以上三个链接时,产生的情况可能有如下几种:
- 页面没有变化:访问三个链接,显示的页面没有任何不同。这种情况说明后台针对此查询点的过滤比较严格,是否存在 SQL 注人漏洞还需进行后续测试。
- 页面中少了部分内容:如访问前两个链接正常,第三个页面里有明显的内容缺失,则基本可以确定有漏洞存在。接下来就需要检测是否有union显示位,如果没有,也可尝试进行
bool
注人(详情参见后续关于盲注的介绍)。 - 错误回显:如果访问第三个链接后出现数据库报错信息,那么可以判定当前查询点存在注人,用标准的回显注人法即可实现 SQL 注人攻击。
- 跳转到默认界面:如果第一个链接显示正常,第二、第三个链接直接跳转到首页或其他默认页面,那么这可能是后台有验证逻辑,或者是有在线防护系统或防护软件提供实时保护。之后可尝试绕过防护工具的思路(大小写混用、编码等)。
- 直接关闭连接:如果在访问上述第二、三个链接时出现访问失败,那么这种情况下可尝试利用 Burpsuite 抓取服务器响应包,观察包头 server 字段内容。根据经验,这种情况通常为防护类工具直接开启在线阻断导致,后续可利用编码、换行等方式尝试绕过(极难成功)。
1.3.2 通过回显位确定字段数
回显位指的是数据库查询结果在前端界面中显示出来的位置,也就是查询结果返回的是数据库中的哪列。在 SQL 注人中,一般利用order by、union 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
库中主要涉及内容可参考下图。
在 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 秒
- benchmark(count,expr):重复计算
mid()
和substr()
都是substring
的同义词。语法是:mid(str,pos,len)
,str
是字符串,pos
是起始子字符串的位置(第一个字母为1
),len
是一个可选参数,它决定从起始位置返回的字符个数。例:
SELECT MID('I drink coffee', 3, 5) Result;
结果为:drink
1.5 常见防护手段及绕过方法
SQL 注人的防护方法包括:
- 参数过滤
- 预编译处理
参数过滤分为:
- 数据类型限制(类型和长度)
- 危险字符处理
通俗地说就是:要么严防死守,细致检查;要么严格限定参数的有效范围(参数化查询)。总之就是要尽可能限制用户可提交参数的类型。
针对 SQL 注入设计防护体系时,一定要与真实的业务场景进行配合,很多时候用简单的方式可获得非常好的防护效果。首先需尽可能详细地限制允许用户输人的参数类型及长度;其次,需考虑用户输人内容的特点及目的,开展有针对性的关键字、词过滤;如果是新建系统,推荐利用参数化查询手段,以实现更好的防护效果。当然,在这期间,应尽可能保证中间件版本的更新频率,可有效防护各类型攻击。以下将针对每种防护场景进行探讨。再次强调,防护力较弱的方法并不一定不适用,必须与实际环境相结合来选择。
1.5.1 参数类型检测及绕过
如链接:xxxx.php?type_id=4&new_id=690
,其中type_id=4
和new_id=690
为数字类型且长度均为一定值,均为用户可控,避免参数中出现非数字类型字符,并且对长度进行限制。或使用正则表达式可有效避免 SQL 注入。
防护方法:
- 使用函数对参数类型进行过滤
- 正则表达式
参数类型检测主要面向字符型的参数查询功能,可以用以下函数实现:
int intval ( mixed $var [, int $base = 10 ] )
:通过使用指定的进制 base 转换(默认是十进制),返回变量var
的integer
数值。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 危险参数过滤及绕过
防护方法:
常见的危险参数过滤方法包括关键字、内置函数、敏感字符的过滤,其过滤方法主要有如下三种:
黑名单过滤
将一些可能用于注人的敏感字符写人黑名单中,如
'
(单引号)、union
、select
等,也可能使用正则表达式做过滤,但黑名单可能会有疏漏。白名单过滤
例如,用数据库中的已知值校对,通常对参数结果进行合法性校验,符合白名单的数据方可显示。
参数转义
对变量默认进行adsalashes (在预定义字符前添加反斜杠),使得SOL注人语句构造失败。
GPC 过滤
GPC 是GET、POST、COOKIE 三种数据接收方式的合称。在 PHP 中,如果利用
$_REQUEST
接受用户参数,那么这三种方式均可被接收。在早期PHP中,GPC过滤是内置的一种安全过滤函数,若用户提交的参数中存在敏感字符单引号'
、双引号"
、反斜 线\
与NUL
(NULL
字符),就在其前端添加反斜杠。
由于白名单方式要求输出参数有着非常明显的特点,因此适用的业务场景非常有限。总体来说,防护手段仍建议以黑名单+参数转义方式为主,这也是目前针对 SQL 敏感参数处理的主要方式。
绕过方法:
利用参数变化绕过黑名单防护,白名单没有很好的方法绕过,利用宽字节注入绕过 GPC 过滤。
1 | {% tabs 参数绕过方法 %} |
1.5.4 针对过滤的绕过方式汇总
(1)尖括号(大于小于)过滤绕过方式
用于替代大于>
或小于<
符号或等号=
主要使用函数:between
、greatest
。
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
。