SQL注入之二次注入实战"

admin 2026-11-09 18:54:52 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

SQL注入之二次注入实战

原创

炫彩之星
炫彩之星

菜鸟学安全

2026年11月9日 08:30
广东

去阅读

你转义了输入,但它还会从数据库里再出来一次

常规注入打不动的时候,很多人第一反应是“有 WAF、有过滤,注入这条路算了”。但有一种注入根本不在第一层:你输入的脏字符串被原样存进数据库,等某个列表页、详情页或者重置密码功能把它读出来,再拼进下一条 SQL——这时候没有任何过滤,因为系统默认“从自己库里读出来的数据是干净的”。这就是二次注入,扫描器扫不到它,因为两次请求单独看都是正常业务。

还有一层更隐蔽的,叫二次编码:入口把 %27 过滤了,但 urldecode 放在了转义之后调用,你传 %2527,第一层解码成 %27 顺利通过校验,第二层再解一次就变成了 '。

这篇用 sqli-labs 的 Less-24 走一遍完整链路:注册一个 admin'#,重置密码时把 admin 的密码改掉;再改代码加一个 userlist.php,把真实环境里“从库里取值再拼接”的场景搭出来,最后用 union 直接把用户名和密码拖出来。

一、什么是二次注入和二次编码

简单地说,二次注入是指已存储(数据库、文件)的用户输入被读取后,再次进入 SQL 查询语句所导致的注入。网站对我们输入的一些重要关键字做了转义,但我们构造的语句已经写进数据库,可以在没有被转义的地方使用。可能每一次注入都不构成漏洞,但如果串起来用,就可能造成注入。

普通注入:

(1)在 http 后面构造语句,立即直接生效;

(2)很容易被扫描工具扫到。

二次注入:

(1)先构造语句(带被转义字符的语句);

(2)恶意语句存入数据库;

(3)第二次构造语句,结合前面已经存入数据库的语句,成功——因为系统没有对已经存入数据库的数据再做检查;

(4)更加难以被发现。

二次编码这里没有实验演示,因为靶场没有相应的练习,简单介绍一下原理:当我们输入 %2527 时,浏览器会把 %25 解码为 %,之后不再编码,最终成为 %27;再经过一次解码就变成了 ',注入就成立了(%27 == ')。

方法是在注入点后键入 %2527,然后按正常的注入流程开始注入。

测试方法:

黑盒测试——在可能的注入点后键入 %2527,之后进行注入测试。

白盒测试——1、是否使用了 urldecode 函数;2、urldecode 函数是否在转义方法之后调用。

二、二次注入

1. 二次注入方法

我们用 sqli-labs 24 关练习,是一个登录界面,随便登录一个用户 admin,出现报错了。

我们重新创建一个账号看看,注册 admin'#:

账号注册成功,重置密码成功:

这时看到 admin 账号的密码变为了 1234——admin'# 这个账号的密码本来是 123,重置时改成了 1234,但实际被改的是 admin。这就是注入代码后未经过处理、被二次利用的结果:# 后面的内容被注释掉,' 与前面的 ' 闭合,重置语句直接作用到了 admin 身上。

2. 二次注入环境搭建获取数据库信息

上面的实验不太能体现二次注入的真实环境,我们改代码搭建环境做更深层的练习。

(1)先把 users 表中的 username 字段改大点,便于注入语句,在 mysql 终端执行:

alter table users change username username varchar(255) character set gbk collate gbk_chinese_ci not null;

(2)在 sqli-labs 的 24 关目录下新增 userlist.php,文件内容如下:

<?php
include(\"../sql-connections/sql-connect.php\");
error_reporting(0);
$sql=\"SELECT * FROM users ORDER BY id asc\";
$result=mysql_query($sql);
$num=mysql_num_rows($result);
for($j=0;$j<$num;++$j)
{
    $row=mysql_fetch_array($result);
    $username=$row[1];
    $sql_detail=\"SELECT * FROM users where username='$username'\";
    $result_detail=mysql_query($sql_detail);
    $num_detail=mysql_num_rows($result_detail);
    for($i=0;$i<$num_detail;++$i)
\t\t{
        $row_detail=mysql_fetch_array($result_detail);
\t\techo <<<END
          <table border=\"1\" style=\"table-layout:fixed;\" width=\"1000\">
          <tr>
             <th>$row_detail[1]</th>
             <th>$row_detail[2]</th>
           </tr>
          </table>
END;
    }
 }

?>

(3)做实验前先对 userlist.php 的代码做一次审计:

注入点就在这里——从数据库取出的 username 被直接拼进了下一条查询:

$username   = $row[1];
$sql_detail = \"SELECT * FROM users WHERE username='$username'\";   // 注入点

在注册页面注册一个新用户,用户名填:

1' union select 1,user(),3#

打开 userlist.php:http://192.168.222.4/sqli-labs/Less-24/userlist.php

当前数据库用户的信息出现了。原理按代码推一遍就明白:

$username   = 1' union select 1,user(),3#;

$sql_detail = \"SELECT * FROM users where username='1' union select 1,user(),3#'\";

最终执行:SELECT * FROM users where username='1' union select 1,user(),3#

实际执行到 # 就结束了,后面那个 ' 被注释掉,回显 1,root@localhost,3。

最后再注入一次,把表里的数据拖出来,同样先注册:

1' union select 1,(select concat(0x7e,username,0x7e,password) from security.users limit 0,1),3#

成功获取数据,实验结束。

写在最后

二次注入最难受的地方在于它绕开了所有人的直觉:扫描器只在单个请求里找特征,而这里的两次请求拆开看都干净得像正常业务;开发人员做了转义,只不过转义只发生在入口那一层。脏数据一旦落库,它就被默认为可信,后面每一次被取出、拼接、再执行,都没有人再检查它。

所以这类漏洞更多靠人而不是靠工具。白盒视角,先在代码里找“从数据库取值后直接拼进 SQL”的地方,$username = $row[1] 后面紧跟一个 SELECT ... where username='$username' 就是教科书式的靶点;黑盒视角,记住两个动作——注册或提交时用 '# 这种闭合符试一次,然后在列表、详情、重置密码这些“二次回显”的地方逐个看,有没有报错、有没有字段串位、有没有本该显示 A 的数据变成了 B。

防御上就三件事:全链路参数化,出口也算入口,别只守第一层;urldecode 的顺序别放反,解码必须发生在校验之前;把所有 SQL 报错关掉,别让 0x7e 这种拼接结果直接从页面漏出来。

转义从来不是终点,它只是第一道门。真正的安全边界,得把“数据从库里回来”也算进去。

    评论:0   参与:  0