文章目录
- 1.前言
- 2.实验环境搭建
- 3.原理介绍
- 3.1 HTTP协议
- 3.1.1 HTTP报文
- 3.1.2 GET方法
- 3.1.3 POST方法
- 3.1.4 从页面到报文
- 3.2 SQL注入
- 3.2.1 GET注入
- (1)验证GET传参
- (2)SQL注入
- 3.2.1 POST注入
- (1)验证POST传参
- (2)SQL注入
- 4.More
1.前言
SQL注入是Web安全层面最高危的漏洞之一,长期霸榜OWASP Top10首位,但是究竟什么事SQL注入?SQL注入又是怎么产生的?接下来本篇文章将详细介绍SQL注入产生的原理。本篇文章并没有描述具体的注入方法,而是侧重于对原理的描述,并分别分析了GET和POST注入。
2.实验环境搭建
在弄清SQL注入的基本原理之前,我们首先要来搭建实验的环境,我们需要一台L(Linux)NMP服务器或者W(Windows)NMP服务器,具体服务器的搭建可以参考我之前的文章《一篇文章告诉你如何搭建LNMP》,或者直接使用phpStudy完成搭建。
完成服务器的搭建后,我们要准备好测试用的数据库,执行下面的SQL命令完成数据库的建立
create database SQLInjection;
use SQLInjection
create table users(id int,username varchar(255),password varchar(255));
创建完成后,可以插入几条用于测试的数据
insert into users value(1,'Tom','123456');
insert into users value(2,'Alice','654321');
insert into users value(3,'Jerry','qwerty');
3.原理介绍
3.1 HTTP协议
HTTP协议是Web应用层协议,全称是超文本传输协议。这个协议是Web应用的核心,HTTP由两个程序实现,一个是客户端程序,通常是我们使用的浏览器,一个是服务器程序,运行在我们所访问俄的服务器上。这两个程序通过HTTP协议进行交流,HTTP定义了这些报文的语法、语义和时序。这里我们并不需要对HTTP协议做过深的了解。但是我们依旧需要明确以下几个知识点:
1.客户端程序与服务器程序之间是通过报文进行交流的,通常是客户端请求数据,服务器响应客户端请求的数据;
2.报文是包含了不同的字段,不同的字段代表的不同的信息
3.报文中携带了客户端程序与服务器程序之间通信的数据
4.数据在报文中的位置不同,服务器接收该数据的方式也不同
3.1.1 HTTP报文
客户端与服务器端的报文,根据发送方的不同被分为请求报文和响应报文,从客户端发往服务器端的被称为请求报文,从服务器端发往客户端的被称为响应报文。请求报文和响应报文都有不同的固定格式,请求报文的一般报文格式如下
响应报文的一般格式如下
因为本文主要主要讲述的是SQL注入,SQL注入我们是无法操作服务器的,只能通过影响从客户端发送的请求报文进而从服务器获取数据,所以我们只需要了解请求报文,在上面的请求报文结构图中,我们可以看到请求报文主要包含请求行、首部行、空行、实体体4个模块。各个模块的功能说明如下
模块名 | 说明 | |
---|---|---|
请求行 | 方法 | 说明传递数据的方法 |
URL | 指明想要访问的文件 | |
版本 | 协议的版本HTTP /1.1 | |
首部行 | 首部行中包含众多的字段,不同的报文可能会携带不同的字段,在请求报文中主要用来说明客户端的相关信息 | |
空行 | ||
实体体 | 当使用POST方式传递数据的时候,该模块会填充POST想要传递的数据 |
在前面的内容中不断的提到数据的传递,数据传递的方法会在请求行的方法中说明,主要有4种数据传递的方法,这里只介绍两种与SQL注入有关的
3.1.2 GET方法
当请求报文的方法字段被设置为GET的时候,表明该请求报文使用GET方式进行传参。通常GET方法是用来获取服务器上的指定文件,比如
GET /index.php HTTP/1.1
上面的请求行用来获取网站根目录下的index.php文件,当站点接收到该请求后会将用户请求的文件内容进行解析,然后通过响应报文响应用户的请求。但是也会遇到某些文件需要接收参数的情况,客户端程序会使用下面的方式对文件完成传参
GET /index.php?id=1 HTTP/1.1
这样服务器上的文件便能够接受id=1的传参。然后根据用户的传参作出响应的处理。
3.1.3 POST方法
当请求报文的方法字段被设置为POST的时候,表明该请求报文使用POST方式进行传参。通常POST方式用来提交用户输入的表单信息,这些协议存放在请求报文的实体体中。比如
POST /user/login.html HTTP/1.1
//......
user=admin&pass=admin
上面是一个用户登录报文的示例,通过POST方式提交用户输入的用户名和密码。
3.1.4 从页面到报文
上面所说到的报文或者传参方式都是客户端程序生成的,作为用户只需要在页面上点击鼠标或者输入内容即可。那么客户端程序是怎么确定要使用什么样的方式传参呢?简单来说我们在表格中输入的数据会通过POST进行传参,而我们的点击页面的操作一般会通过GET传参。
POST传参的方式比较好理解,但是我们还要重点说明一下GET方式,比如当我们浏览某个站点的时候,点击了站点中的一篇文件,页面就给我们显示了我们点击的文章。现在我从数据包的角度分析站点为什么可以准确的显示我们所点击的文章。首先我们点击文章,服务端会发送类似于下面的数据包
GET /index.php?p=1234 HTTP/1.1
Host: www.test.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html, **;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=0871f074b43a28eb3e15dcf7b0635b6d
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
可以看到该数据包使用GET方式进行传参,并在URL字段指定了文件的路径和我们对文件的传参。同时返回的页面显示了我们在URL栏中传递的参数
上面的操作也就验证了通过URL栏的方式传递参数是GET方式的传参。并且测试了我们的文件可以接受GET传参的内容。
(2)SQL注入
将上面的代码修改为
echo 'hello,world!'.'<hr/>';
#1.获取get传参
$id=$_GET['id'];
echo '你传入的参数'.$id.'<hr/>';
#2.链接数据库
$conn=mysqli_connect('localhost','root','root','sqlinjection');
if(!$conn) die("<script>alert('Sorry,cannot connect to database')</script>");
#3.构造SQL命令
$sql="select * from users where id=$id";
echo '执行的SQL语句'.$sql.'<hr/>';
#4.查询数据库
$result=mysqli_query($conn,$sql);
#5.显示查询结果
echo '查询结果'.'<br/>';
while($row=mysqli_fetch_array($result))
{
echo 'id:'.$row['id'].';'.'username:'.$row['username'].';'.'password:'.$row['password'].'<br/>';
}
#6.断开链接
mysqli_close($conn);
代码修改完成后,我们接着访问该代码,然后传入参数id=1,页面显示如下
可以看到站点成功的接收了我们的传参,并根据我们的传参访问数据库,并显示了数据库的访问结果。这是正常情况下的传参,可以理解为我们登录用户后,页面通过GET传参的方式从数据库中取出我们的用户信息显示在页面上。因为我们可以控制传参的内容,那我们尝试将URL栏修改为
http://192.168.25.147/sqlinjection/test.php?id=1 union select * from users where id=2
站点显示的内容变成了
也就是说我们修改URL栏中的内容将id的值变成了
id=1 union select * from users where id=2
站点在接收到该值后,将该变量的值与SQL命令进行拼接,形成了如下的SQL命令
select * from users where id=1 union select * from users where id=2
随后执行了该SQL命令,显示了预期之外的数据。
在正常操作下id接收的是一个数字,然后将该数字拼接到SQL命令中执行。但是我们通过人为修了id传参的值,也就是说我们可以控制id的值,并将其修改为一个恶意的值;接着系统会将我们输入的内容拼接道SQL命令中,即待执行的代码拼接了用户输入的内容;最后拼接了用户输入内容的代码执行了,获取了数据库中的数据。
3.2.1 POST注入
理解了前面的GET注入的原理,POST注入的原理和GET完全一致,不同点在于传参方式的不同。
(1)验证POST传参
准备了如下的程序将其放置在sqlinjection目录下
login.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="login.php" method="POST" target="_self">
username:<input type="text" name="username"/><br />
password:<input type="password" name="password"/><br />
<input type="submit" value="login"><hr />
</form>
</body>
</html>
login.php
<?php
#1.从前端获取数据
$username=$_POST["username"];
$password=$_POST["password"];
echo '你输入了如下的内容'.'<br/>';
echo 'username:'.$username.'<br/>';
echo 'password:'.$password.'<hr/>';
?>
随后我们访问login.html,页面显示如下
输入如下的内容后
字段名 | 内容 |
---|---|
username | Tom |
password | 123456 |
点击login并抓取数据包,得到的数据包内容如下
POST /sqlinjection/login.php HTTP/1.1
Host: 192.168.25.147
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:76.0) Gecko/20100101 Firefox/76.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
Origin: http://192.168.25.147
Connection: close
Referer: http://192.168.25.147/sqlinjection/login.html
Cookie: PHPSESSID=0871f074b43a28eb3e15dcf7b0635b6d
Upgrade-Insecure-Requests: 1
username=Tom&password=123456
可以看到数据包使用的是POST的方式,并且在实体体中携带了我们输入的用户名和密码信息。随后页面会显示下面的内容
可以看到我们在表单是输入的内容是使用POST方式携带在实体体中进行传参的。同时login.php能够成功的接收我们传入的数据
(2)SQL注入
将login.php脚本修改为
<?php
#1.接收用户传参
$username=$_POST["username"];
$password=$_POST["password"];
echo '你输入了如下的内容'.'<br/>';
echo 'username:'.$username.'<br/>';
echo 'password:'.$password.'<hr/>';
#2.链接数据库
$conn=mysqli_connect('localhost','root','123456','sqlinjection');
if(!$conn) die("<script>alert('Sorry,cannot connect to database,please login later!')</script>");
#3.构造查询语句
$sql="select * from users where username='$username' and password='$password'";
echo '执行的SQL命令为:'.$sql.'<hr/>';
#4.进行查询并获得结果
$result=mysqli_query($conn,$sql);
$row=mysqli_fetch_array($result);
if(isset($row))
{
echo '---登录成功---'.'<hr/>;';
}else{
echo '---登录失败---'.'<hr/>';
}
?>
当我们输入正确的用户名和密码的时候页面会显示
如果输入的用户名或者密码是错误的时候,页面会显示
也就是说站点能够正确的验证用户名和密码的正确情况,但是我们尝试输入下面的内容
字段名 | 字段值 |
---|---|
username | Tom |
password | ’ or 1=1 or ’ |
我们发现居然也登录成功了
仔细观察我们输入的内容拼成的SQL语句
select * from users where username='Tom' and password='' or 1=1 or ''
在执行select的时候判断条件有4个,分别是
条件1.username='Tom'
条件2.password=''
条件3.1=1
条件4.''
其中条件1与条件2使用and链接,其他的条件使用or链接,当程序从左往右判断条件成立的时候,执行到条件1的时候能找到使条件成立的记录,虽然执行到条件2的时候发现不成立,但是因为后面使用了or连接了一个恒成立的式子1=1,因此重要1=1存在其他的条件就算不成立也可以查找到对应的记录。所以在这里即使我们不是输入密码也可能成功的登录。这也是常用的万能密码。
以上便是POST注入的基本原理,可以看到和GET注入类似。当我们输入用户名的时候可以输入任意的内容,也就是说用户可以控制输入。当程序带着我们输入的内容查找数据库的时候,也就是拼接我们输入的内容并执行。
4.More
更多内容欢迎访问个人网站西西弗斯的巨石