SQL注入原理与实践(实验笔记)

原理:

什么是SQL注入?

所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

具体来说,它是利用现有应用程序,将(恶意的)SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。

我该如何向非技术人解释SQL注入?
PHP基础语法
MySQL基础语法
Mysql创建表时反引号的作用

SQL注入原理

SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序

而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。

实践:

实验代码(MYSQL)

<?php
header("Content-type: text/html; charset=utf-8");

$link = @mysql_connect('127.0.0.1', 'root', '');  // 连接数据库
mysql_select_db('sqli');    // 选择默认数据库为sqli
mysql_set_charset('utf8');  // 设置编码为utf8

if(!isset($_GET['id'])){
    header("Location: sqli-1.php?id=1");    // 如果没有传参,则重定向到这个页面,也就是如果没有传参,默认传参数id=1
}

$id = $_GET['id'];  // 获取用户提交的id参数的值
$sql = "select * from `new` where id = " . $id;  // 拼接SQL语句,由于直接获取用户的输入后,没有任何处理,存在SQL注入
$result = mysql_query($sql);
while($row = mysql_fetch_array($result, MYSQL_ASSOC)){  // 遍历输出查询结果,MYSQL_ASSOC表示mysql_fetch_array返回的结果格式为关联数组形式,即可以使用数据库中的字段作为索引获取值。
    echo '<h2>' . $row['title'] . '</h2>';
    echo '<p>' . $row['content'] . '</p>';
}

代码已经写了注释,如果没有给这个页面传参数,则默认传一个id参数,它的值为1。
可以看到,在13行,获取到用户传过来的参数后,直接进入了SQL语句,带进了查询语句,没有任何其他操作,很明显的存在SQL注入。

在sql语句中,new 被反引号包起来了,关于反引号的作用,请参考预备知识中的“mysql创建表时反引号的作用”。

以前很多的网站的信息公告页面的代码与上面类似,不过现在一般都是用的MVC框架,更加模块化,这里先不说MVC是什么,会在以后的课程里介绍,不知道MVC也完全不影响本次实验。

  在网上搜到的判断网站是否存在注入点最常见的是加单引号,即“’“,
  如果不加单引号页面显示正常,而加了单引号以后,页面显示不正常或者错误,则说明网页存在SQL注入,
  还有一种就是如果 加and 1=1 页面显示正常,
  								加and 1=2页面不正常或者错误,同样说明存在SQL注入
  这是什么原因呢?首先通过这个方法来判断是否存在注入点。

第一步

先在URL后面加一个单引号,可以看到页面报错了
在这里插入图片描述

这个警告的意思是:mysql_fetch_array()这个函数第一个参数期望的参数类型是一个资源集,但是给的是一个布尔值

出现该警告的位置在代码的第15行
在这里插入图片描述
查看代码,发现它的第一个参数$result这个参数的值来自第14行,它是mysql_query执行后的返回值
在这里插入图片描述
查看php官方手册,可以看到 mysql_query在查询出错的情况下会返回false,还有一种情况就是在没有权限访问查询语句中引用的表的时候,也会返回false
在这里插入图片描述
这里明显不会是第二种情况,因为我们没有输入单引号的时候,能正常查询,而我们又没有修改查询的表,所以排除这种情况。

所以可能就是前面的语句在MySQL里执行出错了,于是返回了false,这说明我们传进去的参数1’破坏了SQL语句,我们可以稍微修改下代码,在第14行后面输出一下当前执行的sql语句。修改后的代码如下:
在这里插入图片描述
然后再次访问页面,输出了数据库最终执行的SQL语句,为:select * from new where id = 1’
在这里插入图片描述

有编程经验的可能一眼就能看出来该SQL语句有错误,因为如果单引号或者双引号不是出现在字符串中的时候,单引号、双引号都是成对出现

SQL数据库将单引号解析成代码与数据间的分界线,在单引号外面的内容是需要运行的代码,而单引号引起来的内容都是数据

我们可以直接复制该语句到MySQL中去执行。由于数据库编码是utf8的,直接在mysql客户端中进行查询,中文会显示乱码,所以可以到phpmyadmin中来执行这个sql语句

在这里插入图片描述
然后点击左边的sqli,在右侧的new这一行选择浏览
再选择SQL
在这里插入图片描述复制刚才输出的语句:select * from new where id = 1’,然后点击执行
在这里插入图片描述
返回的错误很明显,引号不配对,语句执行出错,所以mysql_query返回了false

出现这个错误是因为单引号被解析成了字符串分隔符,运行时执行的SQL查询在语法上存在错误,所以数据库抛出异常。

上面的情况说明我们输入的参数被mysql服务器当作SQL命令执行了!

如果是这样的话,那我们是不是可以输入其他字符让数据库执行然后返回我们想要知道的数据呢?
试一下呗

咱们通过改成 and 1=1and 1=2 来判断网页是否存在SQL注入的原理

首先:
(1)传个1 and 1=1,页面显示正常
而且最终执行的SQL语句是: select * from new where id = 1 and 1=1
在这里插入图片描述
然后
再次尝试输入1 and 1=2,页面除了输出最终执行的SQL页面,没有其他任何输出。
在这里插入图片描述
为什么会这样?就算SQL语句执行出错,也应该像上面那样报错不是吗?

其实这里SQL语句执行并没有出错,问题出在and 1=2上
再来看一下这条语句,它从new这个表里面查询数据,但是有限制条件
  
where指查询满足后面的数据,也就是说需要查找id = 1的数据,并且需要 1=2,
但是1=2这是不可能满足的。
  
所以,这个语句被数据库准确无误地执行了,
但是它并没有找到符合条件的数据,所以没有任何数据输出。

而前面加 1 and 1=1能正常返回数据,是因为1=1这个永远为真,
所以这个条件可以说是跟没加一样。

继续去phpmyadmin里验证,分别执行这2条语句
在这里插入图片描述
可以看到,在后面加and 1=1的时候,返回了id=1的数据

然后执行and 1=2,点击显示查询框来重新输入SQL语句
在这里插入图片描述
然后输入select * from new where id = 1 and 1=2,再次点击执行
在这里插入图片描述

可以看到提示返回的查询结果为空,也就是没有返回结果。但是在下面还是显示了一条结果,这是上一条的查询,可以把id改成2试试
在这里插入图片描述
可以看到我们虽然查询语句中的id变成了2,但是下方还是1,说明这不是我们这次查询的结果。

实验总结

通过添加单引号来判断页面是否存在SQL注入,是因为我们添加的单引号被传到了SQL语句中,破坏了原本的SQL语句,还造成了语法错误,所以SQL语句执行,页面返回跟正常的时候会有不同。

而通过and 1=1 、 and 1=2来判断页面是否存在注入,是因为我们修改了原本的SQL语句逻辑,当添加and 1=1的时候,这个条件永远成立,所以页面一定返回正常(需要该页面存在注入),但是当添加and 1=2的时候,这个条件永远不成立,所以如果该页面存在注入,必然不会返回任何数据。所以就会说,如果添加and 1=1页面返回正常,添加and 1=2页面返回不正常或者错误,就说明页面存在SQL注入。当然这只能大概判断,而不是说明该页面一定存在注入,还有其他情况。

提交的参数,改变了原来SQL语句的含义,并且被服务器当成SQL语句执行了,这就是SQL注入!

万能密码

那万能密码又是怎么回事呢? 网上找到的万能密码都是类似’ or 1=1这种,为什么输入这个字符串就可以绕过登录了呢? 我们也用or 1=1试试,看在这个页面提交这个值是什么效果。

访问:http://sqli.com/sqli-1.php?id=1 or 1=1
在这里插入图片描述
发现居然输出了多条记录,这里其实输出了new表中所有的数据

那么为什么这里会输出所有内容呢?

  最终执行的SQL语句是:select * from `new` where id = 1 or 1=1。

  它这里只要满足id =1 或者1=1任意一个条件,而1=1永远为真,
  所以返回了所有的数据,其效果与select * from `new`一样

第二步

如果仅仅只是读取new这个表里面的数据,对我们来说,其实意义不大,
因为即使不存在注入,我们也可以增加ID的值来查看其中所有的信息。

比如访问http://sqli.com/sqli-1.php?id=2 可以查看id=2的内容等

我们希望的是能够通过注入,获取一些网站管理员不希望我们知道的数据,比如管理员的账号密码。

在本页面的源码中,注入点在where后面,也就说,从哪个表中查询什么数据是已经写死的

那么怎么才能通过注入获取其他表中的内容呢? 比如获取user表中的数据:

通过union操作符就可以达到我们的目的,
union可以合并两条或者多条select语句的查询结果,
它的语法如下:

Select column-1,column-2,…,column-N from table-1 union select column-1,
column-2,…,column-N from table-2

它的返回值是两个select语句查询结果组成的表。
我们可以通过在第一个查询后面注入一个union运算符,并添加另外一个任意查询,
就可以读取到数据库中用户可以访问的任何表。

但是,使用union有一些限制:

1.两个查询返回的列数必须型相同。

2.两个select语句对应列所返回的数据类型必须是相同或者是兼容的。

两个查询返回的列数必须相同时什么意思呢?

是指当前的两个select 他们查询的列的个数必须一致,
如果第一个select查询了5列,则第二个select也必须查询5列。

如:select title, content from new union select username, password from user;

像上面的查询,列数相同,会返回这2个查询的结果集。

来到phpmyadmin中验证一下,如图:
在这里插入图片描述
但是如果列数不同,如:

select title, content from new union select username from user;

则会报错,如图:
在这里插入图片描述
很明显的提示查询的列数不同。
那么第二个条件又是什么意思呢?

它的意思是,第一个查询的每一列,与第二个查询的对应的列的数据类型,
至少应该是兼容的

比如,字符串是兼容数字的。但是数字是不兼容一些字母等字符串的,
所以,如果字段的类型是int,但是在往该字段插入字符串数据的时候,就会报错。

但是在union查询的时候,其实这个限制可以忽略不计,几乎所有类型都兼容。

同样来到phpmyadmin中测试,这里只测试常见的类型,有兴趣的可以自行测试所有的类型。
执行以下SQL语句:

SELECT id, title, content FROM `new` union select id, username, password from user

返回结果正常
在这里插入图片描述
查看一下发现new 表中的title字段是text,但是user表中的password字段是char,明显这2个查询的对应列的类型不同,但为什么查询还是没报错呢? 因为text类型是兼容char类型的

为了方便比较,现在我们稍微修改一下SQL语句,只查询2列,但是有一列的类型不兼容,第一个查询的第一列类型为int,但第二个查询的第一列类型为char

修改后的语句如下:

SELECT id, title FROM `new` union select username, password from user

执行后同样没报错,因为int和char同样是兼容的!
在这里插入图片描述
实际的SQL注入应用中,一般第二个select都是用数字或者null,因为这2个类型兼容比较,null更是兼容所有类型。

所以我们这里重点关注怎么知道查询中列的数量
代码中,写的是select * from new,直接在代码中看不出来new这个表中有多少列,而需要去查看建表语句

而在实际应用中,除非是开源的CMS,否则你不可能准确知道它查询了多少列,这就需要我们想个办法来获取当前查询的列数。

一个简单的办法是通过 order by 子句来确定。

Order by 是根据指定的列名进行排序。

Order by 子句可以接受一个列名作为参数,也可以接受一个简单的、能表示特定列的数字,所以可以通过增大order by 子句中代表列的数字来识别查询中的列数。

来到phpmyadmin测试

执行show create table new来查看建表语句

在这里插入图片描述
可以看到这个表有3列
然后来测试用order by子句来测试列数量,从1开始,每次查询加1,加到报错为止,如:select *from new order by 1
在这里插入图片描述
1的时候正常,继续增加该值
在这里插入图片描述

到4的时候,发现执行出错

因为这里一共只有3列,但是你要根据第4列来进行排序,当然就会报错了。

再测试一个语句,把* 改成title, content。还是从1开始测试。
结果如下:

select title, content from new order by 1 

结果正常
在这里插入图片描述

select title, content from new order by 2 

结果正常
在这里插入图片描述

 select title, content from new order by 3 

报错
在这里插入图片描述
因为我们这里只查询了2列。

有了以上知识,我们就可以在不看源码,不看建表语句的情况下,判断当前查询的语句中包含多少列了。

现在就来注入页面进行测试:

访问http://sqli.com/sqli-1.php?id=1 order by 1,页面正常
在这里插入图片描述
访问http://sqli.com/sqli-1.php?id=1 order by 2,页面也正常

依次增加order by 子句后面的值,直到4的时候,页面报错

在这里插入图片描述
所以我们可以得出结论,在这个查询中,一共查询了3列,因为4的时候报错了。

然后就可以通过union 来进行查询user表中的数据啦。

根据union的语法,构造注入语句,
通过前面的测试已经知道这个查询一共查询了3列

在符合union操作符的前提下,可以构造如下语句:

http://sqli.com/sqli-1.php?id=1 union select null, null, null

访问该连接,页面正常,没有任何多余的内容出现,与没加后面的sql语句一样。
但是你查看源码会发现,多了个 < h2>标签和< p>
在这里插入图片描述

因为其实我们这里后面的查询也执行,只是返回的是null,所以在页面没有显示,但是标签还是被数出来了!

虽然后面的语句执行了,但是如何获取我们想要的数据呢?

这就需要我们确定哪个列最终会被输出。所以我们可以把null依次替换成数字或者其他字符。

例如:http://sqli.com/sqli-1.php?id=1 union select 1,2,3

可以看到页面输出2,3,这就说明在前一个查询中,第二列和第三列被输出来了,我们也可以依次替换而不是一次就全部替换成数字,像下面这样:

http://sqli.com/sqli-1.php?id=1 union select 1, null, null

http://sqli.com/sqli-1.php?id=1 union select null, 2, null

http://sqli.com/sqli-1.php?id=1 union select null, null, 3

其中 1、2、3都可以替换成其他数字,需要注意的是,替换成数字以外的字符的时候,字符需要用双引号或者单引号引起来,否则就是SQL语法错误了。

如:
http://sqli.com/sqli-1.php?id=1 union select “test”,”test2”,”test3”

在这里插入图片描述
在第二个select中,如果该列被原样输出了,说明在该列可以用来获取信息,因为通过该列获取的信息会被程序输出。

那么为什么会这样呢?

这是因为mysql的select 在直接select一个字符串常量或者数字的时候,
会直接返回。

这个也可以phpmyadmin中测试。
在这里插入图片描述
注意看查询的字符串与返回值的位置。

通过上面的方法确定输出位置以后,我们就可以用来获取数据啦!

我们可以执行一些数据库函数

如:执行database()来查看当前数据库,执行user()来查看当前连接数据库的用户

在这里插入图片描述
我们的目标是获取user表中的用户名和密码
查看文件,可以看到user表中存在username, password字段
在这里插入图片描述
所以就可以构造如下语句:

http://sqli.com/sqli-1.php?id=1 union select null, username, password from user

在这里插入图片描述
这样就获取了user表中所有的用户名和密码。

如果你写的网站存在注入,别人很容易就能通过SQL注入拿到管理员账号密码
那么网站就很容易被人篡改,并且导致信息泄露

第三步

在实验步骤二中,是通过查看建表语句知道表中存在什么字段的,那么如何在不知道建表语句的情况下,通过注入来获取当前连接数据库的账号所有有权限访问的数据库名、表名以及字段呢?

这就需要了解mysql数据库的information_schema 这个数据库了。

根据从“SQL注入中information_schema的作用”实验提到的技术,我们可以很容易就构造出获取所有数据库的语句,为:

http://sqli.com/sqli-1.php?id=1 union select 1,schema_name,3 from information_schema.schemata。

可以看到返回了所有的数据库名:
在这里插入图片描述
其中schema_name 和 数字3的位置可以互换,没有什么影响,因为这里只需要有一个位置输出了就行,其中的数字1和3 可以被其他数字或者字符或者NULL(指针)代替,用字符代替的时候,记得用双引号或者单引号引上。虽然1和3可以被代替,但是不能缺少,因为必须要保持3列union才能正确执行。

这里后面的from 为什么是information_schema.schemata而不是schemata呢?

因为在当前执行SQL语句的环境中,它的默认是库是sqli,如果我们要跨库查询,则需要在表名前加上库名,如果不加上库名,则表示在sqli这个数据库的schemata表中查询,由于sqli这个数据库不存在schemata表,所以会报错
如下图:
在这里插入图片描述
如果我们输出mysql 的报错信息,就可以看到很明显的提示。

修改sqli-1.php源文件,在执行sql语句的下面加入如下代码:

if(!$result){     

    die(mysql_error());

}

修改后内容为:
在这里插入图片描述
再次访问上述链接:
在这里插入图片描述
提示表sqli.schemata 不存在。

通过前面的注入语句,我们已经获取了所有的数据库库名:

information_schema

mysql

performance_schema

sqli

test

在实验步骤二中,我们已经获取了sqli数据库user表中的数据,这次换一个,改成mysql。mysql这个数据库保存了mysql的账号密码信息,在注入的时候,可以读取该表中的数据来读取mysql账号密码。

现在就通过注入来获取mysql的账号和密码。

首先查看mysql这个数据库中有哪些表。

构造如下语句:

1 union select 1, table_name,3 from information_schema.tables where table_schema='mysql'

返回结果如下:
在这里插入图片描述
可以看到有很多表,由于这里表太多,不再截取所有表的图片,拉动浏览器右边的滚动条到最下面,可以看到存在一个user表
在这里插入图片描述
这个表中保存了mysql服务器的账号和密码的hash值。然后构造语句获取该表中的所有列名。构造语句为:

1 union select 1, column_name, 3 from information_schema.columns where table_name ='user' and table_schema='mysql'

访问后返回该表中所有的列名
在这里插入图片描述
存在User字段和Password字段。

注:列名也就是我们常说的字段,他们指的是一个东西。

User字段保存了MySQL服务器的用户名,Password字段保存的是用户的Hash值。

由于在这里有2个位置可以输出,所以我们可以一次查询用户的账号和密码。

构造语句如下:

1 union select 1, User, Password from mysql.user

在这里插入图片描述
发现页面只有输出root。是我们查询失败了么?

查看一下页面源码,把下方滚动条拉到最右方,发现有多余的标签

在这里插入图片描述
这说明确实有输出,这里之所以会这样是因为我们的root密码为空,所以页面显示看不出来。

进入phpmyadmin确认一下
在这里插入图片描述
在phpmyadmin中显示有3条,这跟我们上面查的不同

进入mysql终端看一下,到底有多少条记录:
在这里插入图片描述

在mysql终端里面查询显示了4条数据,但是为什么我们注入的时候只获取输出了2次呢?

这个问题是union导致的。

union默认情况下会消去表中重复行,所以这里的3个root的行只显示了一次,还有一条记录为空也显示一次。
所以我们看到后面有2个多余的<h 2>标签。

那么,如果我们要查询重复的值怎么办呢?

只要早union 后面加一个all即可,当 ALL 随 UNION 一起使用时(即 UNION ALL)
不消除重复行

在注入语句中的union后添加一个all,构造的注入语句为:

1 union all select 1, User, Password from mysql.user

再次提交
在这里插入图片描述
输出了3个root,查看源码可以看到多余的4个<h 2>以及< p>标签
在这里插入图片描述
最后一个

标签的内容为空,是因为在mysql.user中,存在一条空记录,可以查看上面在终端执行sql语句的返回结果的截图,所以输出为空

 本次实验只演示获取mysql.user表中的User和Password字段的值

参考资料

PHP 中的超全局变量
PHP 中预定义变量$_GET
var_dump 函数语法

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值