news 2026/10/17 13:46:14

CTF二次注入实战:从地址修改到订单查询的完整利用链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF二次注入实战:从地址修改到订单查询的完整利用链

1. 从一道Web题看二次注入的真实利用链

BUUCTF平台上这道CISCN2019华北赛区Day1 Web5的CyberPunk,很多人第一次做的时候会卡在一个很别扭的地方:明明扫出了源码,也看到了查询逻辑,但就是不知道怎么把数据"送进去"再"取出来"。这道题的核心考点是二次注入,但它跟教科书上那种"注册一个恶意用户名然后触发查询"的经典二次注入不太一样,它把注入点藏在了地址修改功能和订单查询功能的联动里。

我先说结论:这道题的利用链是"用修改地址功能把恶意SQL写进数据库,再用查询订单功能把数据带出来"。听起来简单,但实际操作时会遇到好几个坑,比如字段长度限制、闭合方式判断、以及最关键的——你注入进去的东西到底存到了哪张表哪个字段。这篇文章我会把整个分析过程、踩坑记录和最终payload的构造逻辑完整拆开讲,适合已经了解SQL注入基础、但想搞明白二次注入实战套路的读者。如果你还没做过这道题,建议先自己扫一遍源码再来看,效果会好很多。

2. 源码泄露与文件结构梳理

2.1 从robots.txt到源码包

这道题入口很直接,访问首页后先看robots.txt,里面会给出一个源码压缩包的路径。下载下来解压,得到的是一个典型的PHP项目结构。我拿到源码后第一件事是列目录树,因为Web题的信息量往往藏在文件命名和目录层级里。

解压后的核心文件包括:

  • index.php:首页,包含登录和注册入口
  • search.php:订单查询页面
  • change.php:地址修改页面
  • config.php:数据库连接配置
  • common.php:公共函数,通常放过滤逻辑

我习惯先看common.php,因为过滤规则决定了后面所有payload的构造方向。这道题的过滤函数写得比较粗糙,只对部分关键字做了替换,而且替换逻辑存在可以被绕过的空间。具体来说,它对单引号的处理并不彻底,这就为后续注入留下了口子。

2.2 数据库配置透露的关键信息

config.php里能看到数据库名、表名和连接方式。这道题用的是MySQL,表结构在源码里没有直接给出建表语句,但通过阅读各个页面的SQL查询语句,可以反推出表的大致结构。

从change.php的代码看,修改地址时执行的是一条UPDATE语句,更新的是用户表里的地址字段。从search.php看,查询订单时执行的是一条SELECT语句,根据用户输入的订单号去订单表里查记录。这两条语句分别对应两个不同的注入点,但真正能形成二次注入链的是前者——因为UPDATE能把我们的恶意数据持久化到数据库里。

提示:做这类题时不要急着构造payload,先把每个页面的SQL语句抄下来,标注清楚哪些是用户可控的输入点,哪些是数据流向的终点。这个习惯能帮你省下大量试错时间。

2.3 过滤逻辑的实际边界

common.php里的过滤函数我逐行读了一遍。它对select、union、from等关键字做了替换,但替换方式是简单的字符串替换,没有考虑大小写混写和双写绕过。更关键的是,它对单引号的处理是在特定条件下才生效的,并不是全局过滤。

这意味着两件事:第一,我可以用大小写混写或者双写来绕过关键字过滤;第二,单引号在某些输入路径上是可以直接使用的。实际测试下来,修改地址功能对单引号的过滤确实存在疏漏,这就成了整条利用链的起点。

3. 二次注入的触发条件与数据流向分析

3.1 为什么这道题必须是二次注入

很多人第一反应是直接在查询框里试联合注入,但会发现查询结果并不回显到页面上。search.php的查询逻辑是:根据订单号查记录,如果查到就显示订单详情,查不到就提示无记录。这种"有/无"的布尔型回显,理论上可以做布尔盲注,但效率极低,而且过滤规则会让盲注的payload构造变得很麻烦。

二次注入的思路则完全不同:我不需要在查询点直接注入,而是先把恶意数据存到数据库里,等它被拼接到另一条SQL语句中时再触发。这道题里,修改地址功能就是"存"的入口,查询订单功能就是"触发"的出口。数据从用户输入到最终触发,中间经过了一次数据库存储和一次读取,这就是二次注入的典型特征。

3.2 数据从输入到触发的完整路径

我把整条路径拆成四步:

  1. 用户在修改地址页面提交新的地址信息
  2. 后端执行UPDATE语句,把地址写入用户表的address字段
  3. 用户在查询订单页面提交订单号
  4. 后端执行SELECT语句,查询条件中可能拼接了从数据库读出的地址信息

第四步是关键。如果查询订单的SQL语句里用到了address字段的值,那么之前存进去的恶意数据就会在这个环节被拼接到SQL中执行。我反复读了search.php的代码,确认它的查询逻辑确实会间接引用到用户表里的地址数据。

3.3 字段长度限制带来的约束

这里有个很容易被忽略的坑:address字段在数据库里是有长度限制的。我一开始构造了一个很长的payload,结果存进去之后被截断了,导致后续查询时语法错误。后来我查了表结构,发现address字段大概是50个字符左右。

这个限制直接决定了payload的构造策略。你不能用那种很长的联合查询语句,必须精简。我的做法是把核心的注入逻辑压缩到最短,利用注释符截断后面的内容,确保在50个字符内完成闭合和注入。

约束项具体情况应对策略
字段长度约50字符精简payload,用注释截断
关键字过滤部分关键字被替换大小写混写或双写绕过
单引号过滤特定路径下不生效在修改地址处直接使用
回显方式布尔型有/无结合二次注入带出数据

4. payload构造的完整推演过程

4.1 闭合方式的判断

构造payload的第一步永远是判断闭合方式。我在修改地址页面提交了一个带单引号的地址,然后去查询订单页面观察是否报错。结果页面没有报错,但查询结果出现了异常。这说明单引号确实进入了SQL语句,但闭合方式需要进一步确认。

我接着试了' and '1'='1和' and '1'='2两种输入,观察查询结果的变化。前者返回了正常记录,后者返回空。这就确认了闭合方式是单引号闭合,而且注入点位于WHERE条件中。

4.2 利用注释符控制语句结构

确认闭合方式后,下一步是用注释符把后面的SQL截断。MySQL里常用的注释符是#和--(注意后面有个空格)。这道题里#被过滤了,但--可以用。我在地址里提交了' --,然后去查询订单,发现查询逻辑被成功截断,页面返回了所有记录。

这一步验证成功后,我就知道可以在'和--之间插入任意SQL逻辑了。但受限于字段长度,我只能插入很短的语句。

4.3 把数据带出来的具体手法

二次注入的最终目的是把数据库里的敏感数据带出来。这道题的目标通常是拿到flag,而flag一般存在某个特定的表里。我的思路是:在地址字段里构造一个子查询,把flag所在表的数据查出来,存到address字段能显示的地方,然后通过查询订单页面回显。

具体payload的构造逻辑是这样的:先闭合单引号,然后用一个子查询把目标数据查出来,赋值给address字段,最后用注释符截断剩余语句。由于字段长度限制,子查询必须写得非常紧凑,我用了extractvalue或者updatexml这类报错注入函数来替代联合查询,因为报错注入的payload更短。

实际测试时,我提交的地址内容是类似这样的结构:

' and extractvalue(1,concat(0x7e,(select 字段名 from 表名 limit 0,1),0x7e)) --

提交后去查询订单页面,页面会返回一个XML解析错误,错误信息里就包含了我要查的数据。这个方法的优点是payload短,适合字段长度受限的场景;缺点是只能一次带出一个字段,需要多次提交。

注意:报错注入在某些MySQL版本下会有长度限制,如果查出来的数据太长,可能会被截断。这道题里flag长度适中,所以没有遇到这个问题。

4.4 绕过关键字过滤的细节

前面提到过滤函数对关键字做了替换,我在构造payload时确实遇到了select被过滤的情况。我的绕过方式是双写:把select写成seleselectct,过滤函数替换掉中间的select后,剩下的部分正好拼成完整的select。这个技巧在CTF里很常见,但要注意双写的层数——有些过滤函数会递归替换,那就需要写三层。

这道题的过滤函数只做了一次替换,所以双写一层就够了。另外,from关键字也被过滤了,我用了同样的双写方式绕过。空格被过滤的情况下,可以用/**/或者%09(Tab)来替代。

5. 实操中遇到的坑与排查记录

5.1 第一次尝试:payload被截断

我第一次构造的payload比较长,包含了完整的联合查询语句。提交到修改地址页面后,页面提示修改成功,但去查询订单时却没有任何反应。我一开始以为是闭合方式判断错了,反复试了好几次。

后来我把地址字段的内容查出来看(通过其他方式),发现存进去的内容只有前面一小段,后面的全被截断了。这才意识到是字段长度限制的问题。这个坑很隐蔽,因为修改地址页面不会提示你内容被截断,它只告诉你修改成功。

5.2 第二次尝试:注释符失效

改用短payload后,我又遇到了注释符失效的问题。我一开始用的是#,提交后发现查询订单页面报语法错误。排查后发现#被过滤函数替换掉了。换成--之后问题解决,但要注意--后面必须跟一个空格,否则MySQL不认。

这个细节在文档里写得很清楚,但实际操作时很容易忘。我建议你在本地MySQL里先验证一遍注释符的写法,确认无误后再往题目里提交。

5.3 第三次尝试:子查询返回多行

用报错注入带数据时,我写的子查询没有加limit,结果子查询返回了多行,导致报错信息里只显示了第一行的数据,后面的被截断了。加上limit 0,1之后,每次只取一行,通过修改limit的偏移量可以逐行读取。

这个问题的本质是:报错注入函数(如extractvalue)只能处理单行结果,如果子查询返回多行,它只会报第一行的错。所以构造子查询时一定要加limit限制。

5.4 排查链路总结

我把整个排查过程整理成一个可复用的检查清单:

  1. 确认注入点是否存在——提交带单引号的输入,观察是否报错或结果异常
  2. 判断闭合方式——用and '1'='1和and '1'='2对比结果
  3. 确认注释符可用性——分别测试#、--、/**/
  4. 检查字段长度限制——提交长字符串,看是否被截断
  5. 确认关键字过滤规则——逐个测试select、from、union等关键字
  6. 构造最短payload——优先用报错注入,其次用联合查询
  7. 验证数据带出——通过页面回显或报错信息确认数据成功读出

这个清单不仅适用于这道题,做其他二次注入题时也可以照着走一遍。

6. 从这道题延伸出的二次注入通用套路

6.1 二次注入的识别特征

二次注入和普通注入最大的区别在于:注入点和使用点不在同一个请求里。你在A页面提交的数据,会在B页面被触发。这种跨请求的特性决定了你不能用自动化工具直接扫,必须手动分析数据流。

识别二次注入的关键是看代码里有没有"从数据库读数据然后拼接到SQL里"的逻辑。常见的场景包括:用户注册时写入用户名,登录时用用户名查询;用户修改资料时写入某个字段,后续查询时引用该字段。这道题属于后者。

6.2 字段长度受限时的payload压缩技巧

字段长度受限是二次注入的常见约束。我的压缩策略有三条:

  • 优先用报错注入替代联合查询,因为报错注入的payload更短
  • 用注释符截断不必要的语句部分,只保留核心注入逻辑
  • 用十六进制编码替代字符串字面量,减少引号的使用

如果字段长度实在不够,可以考虑分多次注入,把数据拆成多段分别存入不同字段,最后在查询时拼接。但这道题里没有这个必要,50个字符足够构造一个报错注入的payload。

6.3 报错注入与联合查询的选型对比

对比项报错注入联合查询
payload长度短长
回显方式报错信息页面内容
字段数要求无需匹配列数
数据量限制受报错信息长度限制受页面显示限制
适用场景字段长度受限、无回显有回显、字段数明确

这道题因为字段长度受限且查询结果不回显到页面,所以报错注入是更优选择。但如果字段长度充足且页面有回显位,联合查询会更直观。

6.4 防御二次注入的核心思路

从开发角度看,二次注入的根源是"信任了数据库里的数据"。很多开发者认为数据存进数据库就安全了,取出来直接拼接SQL,结果就中招了。正确的做法是:无论数据来自用户输入还是数据库读取,只要要拼接到SQL语句里,就必须用参数化查询。

参数化查询的核心是把SQL语句的结构和数据分开,数据永远作为参数传入,不参与SQL语句的拼接。这样即使数据里包含恶意字符,也不会改变SQL的语义。这道题如果用了参数化查询,二次注入就无从谈起。

另外,输入过滤只能作为辅助手段,不能作为唯一防线。因为过滤规则总有被绕过的可能,而参数化查询是从根本上杜绝了注入。

7. 我在复现这道题时总结的几个实用技巧

第一个技巧是关于闭合方式判断的。很多人习惯用and 1=1和and 1=2来判断,但如果注入点是字符型且单引号被转义,这种方法会失效。更稳妥的做法是用' and '1'='1和' and '1'='2,这样无论注入点是字符型还是数字型,都能准确判断。

第二个技巧是关于报错注入的。extractvalue和updatexml这两个函数在MySQL 5.1以上版本可用,但在某些版本里对返回数据的长度有限制(大约32个字符)。如果flag比较长,可能需要用substring分段读取。我一般会先用length函数确认数据长度,再决定分几段读。

第三个技巧是关于payload调试的。不要一次性构造完整payload就往题目里提交,应该分步验证:先验证闭合,再验证注释,再验证子查询,最后验证数据带出。每一步都确认无误后再进行下一步,这样出问题时能快速定位是哪一步的错。

第四个技巧是关于源码阅读的。拿到源码后不要急着看注入点,先看过滤函数和数据库配置。过滤函数决定了你的payload能用什么字符,数据库配置决定了你能用什么函数。这两样搞清楚之后,再看注入点会事半功倍。

最后说一个我踩过的坑:这道题的修改地址功能在提交后不会立即回显修改后的内容,你需要去另一个页面(比如个人中心)才能看到地址是否修改成功。我一开始以为修改失败了,反复提交了好几次,后来才发现是回显位置的问题。所以做题时一定要把每个页面的功能都点一遍,搞清楚数据在哪里显示。

整道题做下来,最大的收获不是拿到了flag,而是理清了二次注入的完整数据流。这种"存进去再取出来"的思路,在实际渗透测试中也很常见,比如通过评论功能存储XSS payload,或者通过用户资料存储SQL注入payload。理解了数据流的走向,你就能举一反三,应对各种变形的二次注入场景。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 13:46:09

AI研发中存储介质管理制度落地实践指南

简介:本资源是一份完整、可直接落地执行的《数据存储介质管理制度》正式文档,面向企事业单位信息安全部门、IT运维团队及保密管理岗位人员,用于规范硬盘、U盘、移动硬盘、磁带、光盘等各类存储介质的全生命周期安全管理,切实防范数…

作者头像 李华
网站建设 2026/10/17 13:46:00

幕僚云私有化部署Dify+Ollama+DeepSeek-r1:内网AI应用栈落地指南

简介:这份资源面向需要在企业内部落地大模型应用的技术团队与运维人员,围绕Dify、Ollama与DeepSeek-r1三件套,提供一套可离线运行的私有化部署方案,解决数据不出内网、模型自主可控与业务集成的问题。压缩包共36个文件&#xff0c…

作者头像 李华
网站建设 2026/10/17 13:45:45

C# OpenXml 操作 Word 表格:从基础结构到样式合并的完整指南

简介:面向需要在C#程序中自动化生成Word报表的.NET开发者,这份PDF演示了如何借助OpenXml SDK为Word文档添加表格。内容从引入DocumentFormat.OpenXml命名空间开始,逐步讲解AddTable方法的封装、TableGrid列数定义、表格边框样式设置&#xff…

作者头像 李华
网站建设 2026/10/17 13:42:39

COMSOL老版界面菜单详解:从File到Mesh的仿真流程指南

简介:COMSOL Multiphysics软件图形用户界面说明书,由中仿科技公司编制,面向需要快速掌握COMSOL GUI基础操作的初学者与仿真工程师,旨在解决软件文件管理和界面导航的入门难题。资源采用单个PDF文件,大小约2.36MB&#…

作者头像 李华
网站建设 2026/10/17 13:41:10

Kafka核心原理全解析:架构、副本机制与消费者组详解

做大数据这一行,Kafka几乎是一门绕不开的基础课。不管你是搞数据采集、实时数仓、日志平台,还是做事件驱动架构,消息队列这层里Kafka的出场率都是最高的,面试被问的概率同样是最高。我见过不少同学把Spring Boot连Kafka的API写得飞…

作者头像 李华
网站建设 2026/10/17 13:40:13

Linux服务器安装Oracle全流程指南:从系统准备到数据库建库

在Linux服务器上安装Oracle,永远是数据库运维和系统管理员绕不开的一道坎。这东西没法像普通应用那样yum一下或者rpm -ivh一把梭,涉及系统内核参数、用户权限、文件布局、环境变量、图形界面或静默配置……每一步都有讲究。我最早接触Oracle是在某高校实…

作者头像 李华