关于漏洞我最先想到的就是sql注入了,就从它开始吧。大概总结一下知识点。

产生原因 & 简单演示

之所以会产生SQL注入,就是因为开发者过于信任用户的输入。把用户的输入直接拼接到sql语句中执行,这里用sql-labs中的源码做个演示

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
<?php
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
// take the variables
if(isset($_GET['id']))
{
$id=$_GET['id'];
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'ID:'.$id."\n");
fclose($fp);

// connectivity


$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

if($row)
{
echo "<font size='5' color= '#99FF00'>";
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo 'Your Password:' .$row['password'];
echo "</font>";
}
else
{
echo '<font color= "#FFFF00">';
print_r(mysql_error());
echo "</font>";
}
}
else { echo "Please input the ID as parameter with numeric value";}

?>

主要漏洞点就是:

1
2
3
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

这里把用户输入的东西直接插入要执行的sql语句,导致我们可以控制sql语句
如果我们输入:

1
2
3
4
5
$id = ' union select 1,database(),3 --+

#实际执行sql语句

===> SELECT * FROM users WHERE id='' union select 1,database(),3 --+' LIMIT 0,1

这里我们做的事情就是:

  1. 一个单引号',闭合了原本存在的单引号,这样我们后面再输入什么东西就是在单引号包裹的外面了
  2. 最后跟一个注释符 --/#,用来把后面的一些没用的还可能导致报错的语句注释掉
  3. '注释之间添加我们要构造的东西(如这里我们构造了union查询语句,成功把数据库名得到)
    执行结果:

尝试修复

黑名单顾虑

比较简陋的过滤会使用黑名单,过滤一些常见的 sql注入 语句,如:

1
2
3
4
5
6
1.常见关键字
select, insert, update, delete, union, into, load_file, outfile, dumpfile, and, or, xor, where, order, by, group, limit, drop, table, database, schema, grant, revoke, distinct, from, join

2.特殊符号
', ", #, --, /*, */, ;, =, <, >, (, ),, ,, +, -, @, %, , |, &, ^, ~`

绕过

对于黑名单过滤的绕过,一般很难做到防御完全,所以肯定会有开发者没有注意到的关键词,我们一般用关键字替换即可绕过。还有一些技巧例如:

  1. 复写绕过,用于代码将我们的关键词替换为空时。sele select ct(这里空格是为了更好的演示,实际情况不需要空格)
  2. 大小写替换,因为sql语句对大小写不敏感,黑名单很容易过滤了select,但是没过滤SElect,但是在sql看来二者是一样的

白名单过滤

白名单就严格很多了,会只允许出现特定的关键词,基本上如果给的严格的话是绕过不了的

预编译

什么是预编译

预编译语句是一种在执行 SQL 语句之前,将 SQL 语句的结构和查询逻辑预先处理的技术。它与直接执行 SQL 语句相比,具有以下优点:

  1. 防止 SQL 注入:预编译语句将 SQL 语句的结构和数据分开处理,数据作为参数传递,而不是直接嵌入到 SQL 语句中。这种方式有效地防止了 SQL 注入攻击。
  2. 提高性能:当使用预编译语句时,数据库可以缓存和重用查询的执行计划,从而提高查询性能,尤其是在相同的查询结构但不同的参数被多次执行时。

预编译的工作流程

  1. 准备语句:SQL 语句的结构被发送到数据库服务器,数据库服务器解析并预处理这些语句,生成一个查询执行计划。
  2. 绑定参数:在预编译阶段,参数占位符(如 ? 或 :param)被用作数据的占位符,实际的参数值在执行阶段绑定到这些占位符。
  3. 执行语句:将实际的参数值传递给预编译语句,然后执行查询,数据库使用之前生成的执行计划来处理实际的数据。
  4. 获取结果:查询执行后,结果返回给 PHP 脚本,可以进一步处理或显示。

代码示例

使用 PDO 的预编译语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// 创建 PDO 实例
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'username', 'password');

// 预编译 SQL 语句
$stmt = $pdo->prepare('select user_info from users_account where `key` = :key');

// 绑定参数
$stmt->bindParam(':key', $key);

// 设置参数值
$key = $_GET['key'];

// 执行查询
$stmt->execute();

// 获取结果
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

print_r($results);
?>

使用 MySQLi 的预编译语句

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
<?php
// 创建 MySQLi 实例
$mysqli = new mysqli('localhost', 'username', 'password', 'testdb');

// 检查连接
if ($mysqli->connect_error) {
die('Connect Error (' . $mysqli->connect_errno . ') ' . $mysqli->connect_error);
}

// 预编译 SQL 语句
$stmt = $mysqli->prepare('select user_info from users_account where `key` = ?');

// 绑定参数
$stmt->bind_param('s', $key);

// 设置参数值
$key = $_GET['key'];

// 执行查询
$stmt->execute();

// 获取结果
$result = $stmt->get_result();
$rows = $result->fetch_all(MYSQLI_ASSOC);

print_r($rows);

// 关闭语句和连接
$stmt->close();
$mysqli->close();
?>

预编译局限性

动态表名和列名

在预编译过程中,预处理器会去检查数据表和数据列是否存在,不存在则会报错。所以数据表和数据列不能被占位符替代。但有时候数据表和数据列会作为参数传递,所以有时会直接sql拼接

order by

order by/group by后面是不能加引号的,所以后面的参数不能预编译

模糊查询

在模糊查询时因为参数不确定,所以不能使用预编译

宽字节注入

因为预编译时会给特殊符号加上/进行转译,所以我们可以通过宽字节来绕过,但是只限于模拟预编译,加上

1
setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

才能开启真正的预编译