news 2026/10/21 19:08:02

SQL注入入门:WHERE子句检索隐藏数据原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL注入入门:WHERE子句检索隐藏数据原理与实战

讲个真实经历:我第一次打开 PortSwigger 的 Web Security Academy,看到 SQL 注入第一题“WHERE 子句注入漏洞利用——检索隐藏数据”时,内心其实是有点不屑的。因为网上到处是“万能密码”的传说,貌似把' or 1=1--塞进登录框就完事了。结果真到了这个靶场里,我愣是卡了快二十分钟,不是因为题目难,而是因为我没搞清楚“WHERE 子句注入”和“登录框注入”在查询构造上根本不是一回事。

这道题非常适合作为 SQL 注入学习的起点,它只干一件事:让你通过修改 URL 里的category参数,把后台原本隐藏的商品数据全部查出来。整个过程不依赖 Burp Suite,浏览器地址栏就够了。对刚入门 Web 安全的人来说,这一关的正确打开方式不是背 payload,而是把“查询是怎么被拼出来的”这件事想明白;对已经工作多年、做业务开发的朋友来说,这道题也是一个很好的自查案例——你在项目里写的每一条 SQL,是否也藏着同样的拼接问题。

1. 为什么我要反复重做这一关:关于“检索隐藏数据”这个考点

1.1 新手入门 SQL 注入后的最大误区

大部分初学者第一次接触 SQL 注入,都是从某段“万能密码”视频开始的:' OR '1'='1打进用户名框,密码乱输,然后登录成功。这确实是 SQL 注入,但它属于典型的“认证绕过”场景,核心逻辑是把登录查询的 WHERE 条件永远置为真。

问题在于,很多人记住了这个 payload,却没理解它为什么会生效。换到 PortSwigger 第一题这种场景,你就发现不对劲了:这里没有登录框,只有一个商品分类筛选页面,URL 长这样:

filter?category=Gifts

页面会展示 Gifts 分类下所有“已发布”的商品。如果没有一点查询构造的基础,你大概率会习惯性输入' or '1'='1,然后发现页面要么报错,要么还是原样,完全不知道下一步该干什么。原因很简单:' or '1'='1这类 payload 通常是为了绕过与密码相关的条件,而这道题的查询条件是“分类 + 发布状态”,两条查询语句的结构完全不同,注入点也不同。

1.2 这道题到底能让你学到什么

这一关的官方名称叫“SQL injection vulnerability in WHERE clause allowing retrieval of hidden data”,翻译过来就是在 WHERE 子句里有注入漏洞,可以检索隐藏数据。它针对的是商品展示场景下的典型查询:

SELECT * FROM products WHERE category = 'Gifts' AND released = 1;

这里released = 1就表示商品已经发布。被隐藏起来的数据,通常就是released = 0的那些商品,它们在平台上存在,但前台不展示。

攻击者的目标,是通过修改category参数,让整条查询的AND released = 1条件失效。最终页面上不仅会出现分类下的商品,还会把原本“未发布、不可见”的商品全部暴露出来。

理解这个目标非常关键,因为“隐藏数据”四个字不是营销话术,而是指业务系统里真实存在、却被查询条件挡住的那部分数据。在现实世界里,这类数据可能是还没上架的商品、还没审批的评论、还没公开的文章草稿,甚至是被软删除的记录。你通过注入把它们检索出来,看到的本不该出现在前台的东西,就是信息泄露的第一步。

1.3 我反复刷这道题的三个原因

第一次做这题,我靠的是网上现成的 payload,一分钟就过了。当时没什么成就感,反而心虚:我知道怎么操作,但不知道每一步在干嘛。后来转行做安全测试,回头看这一关,才发现它值得反复做的原因有三点。

第一,它逼你建立“查询语句可视化”的习惯。每次看到一个参数,先问自己:后端大概会写一条什么样的 SQL?参数落在哪个位置?是字符串单引号里,还是数字不加引号?这个习惯,能帮你判断后续所有 SQL 注入题目的走向。

第二,它是理解“注释符”和“URL 编码”的最佳入门场景。--注释符在这里不是可有可无的装饰,而是让查询变成合法语句的关键。很多人第一次做实验时卡在这一步,原因不是不知道注入,而是不知道怎么让 SQL 语句不报错。

第三,它是通向 UNION 注入、盲注等高阶题型的根基。WHERE 子句是 SQL 查询里最常见的过滤点,你掌握了这里,后面学习“检索其他表的数据”“绕过过滤”“堆叠注入”都会顺手很多。

2. 漏洞原理拆解:一条筛选查询是如何被输入带偏的

2.1 先看后端最可能出现的 SQL 语句

PortSwigger 这道题,你光看页面可能觉得就是一个普通电商网站的目录页。左侧有分类,点一下“Gifts”,URL 就变成filter?category=Gifts。在后端,最常见的实现代码大概是:

$category = $_GET['category']; $sql = "SELECT * FROM products WHERE category = '$category' AND released = 1";

关键就在$category被直接拼接进 SQL 字符串。对于水平一般的开发者来说,这不是段多冷门的代码,但它却恰恰是 SQL 注入的温床。

我们用正常参数走一遍:category=Gifts,得到的查询是:

SELECT * FROM products WHERE category = 'Gifts' AND released = 1;

数据库会先去 products 表里找category等于Gifts的商品,再从中筛掉未发布的,最终返回一组可见商品。

2.2 单引号试探:这一步为什么值得亲自动手

我见过很多人做这道题,上来直接丢OR 1=1,然后页面没反应就懵了。正确的第一步,应该是先用一个单引号试探。

把category=Gifts改成category=Gifts',后端拼出来的语句会变成:

SELECT * FROM products WHERE category = 'Gifts'' AND released = 1;

SQL 语法里,字符串应该由一对单引号包裹。现在出现了连续三个单引号,数据库读到这里就找不到匹配的闭合引号,直接报语法错误。在页面上的表现通常有两种:要么返回 500 错误,要么干脆显示一个异常页面。

这一步极其重要,它确认了两件事:第一,参数确实被拼接进了 SQL 语句;第二,我们能够通过输入改变这条语句的语法结构。只有确认了这一点,后面再考虑怎么构造合法且有效的攻击语句才有意义。很多人跳过试探直接上最终 payload,导致出现“payload 凑巧能跑通但原理完全不懂”的情况,后面换一个场景就歇菜。

2.3 拼入 OR 1=1 之后,查询变成了什么

试探确认了注入点,接下来就是构造合法语句。我们要让整条 WHERE 条件永久为真,并想办法让后面的AND released = 1失效。

在 URL 里输入:

category=Gifts' OR 1=1--

经过 URL 解码后,后端拼成的 SQL 是:

SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1;

注意这里发生了两件事。

第一,'Gifts' OR 1=1是一个永真的条件。因为1=1恒为真,OR 运算符只要有一边为真,整个条件就成立。于是 WHERE 子句不再关心category是什么,所有商品都符合条件。

第二,--是 SQL 的注释符。它把后面原本是' AND released = 1的部分全部注释掉了。注意,--后面通常还需要一个空格,URL 中可以用+或%20来表示。这样一来,数据库真正执行的查询就变成了:

SELECT * FROM products WHERE category = 'Gifts' OR 1=1;

released = 1这个筛选条件直接消失了。查询结果从“Gifts 分类下的已发布商品”变成了“全部商品”,包括那些从未被发布、被刻意隐藏的数据。

2.4 “隐藏数据”的命名逻辑与业务背景

为什么题目叫“检索隐藏数据”?因为商品表里有条字段,通常叫released,表示发布状态。值为 1 代表已经发布,值为 0 代表未发布。

正常用户的查询无论如何都会被AND released = 1挡住,所以未发布商品在前台不可见。但对数据库来说,它们只是普通的行记录,依然在表里面安安稳稳躺着。注入攻击做的事情,本质上不是“凭空变出数据”,而是通过改变查询逻辑,把原本不可见的记录暴露出来。

这个点理解之后,你会意识到“隐藏”并不是真正隐藏,只是应用层用查询条件挡住了它们。防护做得不好,数据库里的每一条记录都可能被一条精心构造的 WHERE 子句翻出来。

3. 一步步拿下实验:从第一次手工试错到看到全部商品

3.1 进入实验环境后的初始确认

这道题在 PortSwigger 上的入口很直白。打开实验室页面后,你会看到一个类似电商网站的界面,顶部有访问地址,还有一个“Access the lab”按钮。点击进去后,浏览器会打开一个独立的靶机实例 URL,每个人的地址都不一样,但路径结构一致:

https://目标实例.web-security-academy.net/

实验首页有几个商品分类链接,比如 Accessories、Gifts、Lifestyle 等。点击任意一个分类,地址栏会出现:

/filter?category=Gifts

在动手之前,我建议你按 F12 打开开发者工具,切到 Network 面板,再刷新一次页面。观察请求,你会看到浏览器向/filter路径发起了一次 GET 请求,参数category=Gifts被明文附在 URL 上。

这一步不是为了炫技,而是为了确认一件事:参数是 GET 参数,直接体现在地址栏里。这就意味着所有实验操作都可以靠单纯编辑 URL 完成,不需要额外的抓包工具。

3.2 第一次试错:制造一个有意的语法错误

实验的第一步,我的习惯是在参数值后面加一个单引号:

/filter?category=Gifts'

把地址栏里的 URL 改完,按回车,页面很可能直接变成一个错误页。在 PortSwigger 这个靶场里,常见的表现是页面提示“Internal Server Error”或者干脆一片空白。

这说明什么?说明后端没有任何参数校验,你的单引号被原样传到了数据库,SQL 解析直接失败。不要把这个当成失败,这恰恰是整个实验最关键的成功信号。

试错之后的思考路径是:既然是category='Gifts'这种结构,单引号破坏了字符串闭合,那我需要把这个 SQL 句子“修好”,让数据库认为这是一条合法查询。怎么修?要么用注释符把后面的内容丢掉,要么再补一个引号让结构重新闭合。这道题选注释符最干净。

3.3 提交核心 Payload:OR 1=1 加上注释符

接下来在地址栏输入:

/filter?category=Gifts'+OR+1=1--

注意这里把空格写成了+,这是 URL 编码的惯用做法。地址栏里如果你直接输入空格,浏览器也会自动帮你转成%20,但手动用+更直观。URL 解码后,实际传给后端的参数值是:

Gifts' OR 1=1--

拼接到 SQL 里,就是我们前面分析的:

SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1;

按下回车后,页面会刷新,商品列表会明显变长。除了原本可见的 Gifts 分类商品,你会看到一些从未在前台出现过的商品,它们的介绍文字里透露着“尚未发布”的气息,比如某些新品预告、内部测试商品等。至此,实验目标达成:隐藏数据被检出来了。

完整链路我用一个表格总结一下,方便你在练习时对照检查:

操作URL 参数写法后端 SQL 结果页面表现
正常访问category=GiftsWHERE category='Gifts' AND released=1只显示已发布商品
单引号试探category=Gifts'WHERE category='Gifts'' AND released=1500 错误或空白页
核心注入category=Gifts'+OR+1=1--WHERE category='Gifts' OR 1=1所有商品,包括未发布的隐藏商品

3.4 提交答案与验证

PortSwigger 实验室有一个“正文下方”的提交区域,里面有一句话,要求你描述“检索到的隐藏商品的名称”。实际上,当你把隐藏商品全部展示出来后,页面里会有一个明显的、名称特殊的商品,比如某款只在后台存在的新品。把它填进去,点“Submit”,这一关就过了。

这里有个细节值得注意:很多人做实验时一看列表变长就急着提交,却不清楚自己提交的商品到底是不是“隐藏数据”。我的建议是,提交前对比一下原始页面。原始 Gifts 分类下没有这款商品,但注入后列表里出现了,那它就是被隐藏的数据。这个“对比”的思维,在后续所有漏洞实验中都会反复用到。

4. 让我踩坑最多的地方:注释写法、URL 编码和绕过直觉

4.1 别把“万能密码”的肌肉记忆硬套到这道题

我第一次做这个实验,直接复制了登录框的经典 payload:

' or '1'='1

结果页面空空如也。因为登录场景的查询是:

SELECT * FROM users WHERE username = '' or '1'='1' AND password = 'xxx'

而商品分类场景是:

SELECT * FROM products WHERE category = 'Gifts' AND released = 1

两者结构不一样。在这道题里,你如果只构造' or '1'='1,拼出来是:

WHERE category = '' or '1'='1' AND released = 1

确实能让前面的 category 条件失效,但因为AND released = 1依然存在,未发布商品还是不会显示。虽然页面可能正常,但你的实验提交阶段会一直失败。

所以,做题前先想明白:当前查询长什么样?我想让哪个条件失效?在 WHERE 子句注入里,通常不是构造一个复杂的恒真表达式,而是更直接的OR 1=1--。

4.2 注释符的三种写法,以及为什么我推荐--+

这道题的显著难点是注释符。--在 URL 里是个很微妙的东西,它本身在 SQL 中表示注释,但在 URL 的 GET 参数里,它只是普通字符。问题出在“注释符后必须跟一个空格”这个要求上。

不同的数据库对注释符的要求不太一样。在 MySQL 里,--后面必须至少有一个空格才能当作注释符;而在 Oracle、PostgreSQL 等数据库中,--本身就是注释符,不强制要求后面有空格。PortSwigger 的靶场环境用的是 Oracle 数据库,所以使用--通常能成功。

但为了保证兼容性和成功率,我在练习时习惯写--+。这里的+在 URL 编码中等价于空格,所以后端收到的就是:

Gifts' OR 1=1--

也就是注释符后面带了一个空格。这样无论目标数据库对空格是否敏感,都能正常注释掉后面的内容。

还有一个容易踩的坑:如果你在地址栏输入category=Gifts' OR 1=1--,浏览器有时候会把空格原样传到地址栏,但某些前端框架或浏览器插件会自动把空格转成%20,甚至可能破坏--的语义。所以我一般不在地址栏直接输入原始空格,而是显式写成+,减少被浏览器“篡改”的概率。

4.3 注入后页面没变化?先按这三步检查

做实验时最难受的,不是不知道 payload,而是 payload 看起来没问题,页面却毫无反应。按照我的经验,按照以下顺序排查基本都能解决。

第一步,检查单引号试探是否成功。如果category=Gifts'没有引起 500 错误或者页面变化,说明参数可能没走 SQL 拼接,或者目标页面压根不展示报错信息。这时候就要考虑是不是数字型注入、POST 参数注入,需要换一种探测方式。

第二步,检查 URL 编码是否被破坏。+在某些后端框架里会被当作空格,但如果你用的是%27表示单引号,%20表示空格,理论上更稳妥。写 URL 时尤其注意,不要把 payload 里的空格变成null或%00,那会导致 SQL 直接报错。

第三步,检查注释符是否真的生效。把返回的页面源码拿来看,如果查询语句被完整回显在错误信息里,你会看到--后面的部分是否被注释掉。如果AND released = 1还在,说明注释符没生效,换用--+或#试试。

这三个排查步骤,我在后续很多 SQL 注入题目里都在用。它不是这道题的专属技巧,而是一套通用的“注入探测-构造-排错”方法论。

5. 从实验靶场走出来的思考:这一点在真实业务中如何发生与收口

5.1 真实世界里的“隐藏数据”通常藏在哪里

很多人打完这个实验室后,心里可能会冒出一个疑问:现实中真有电商网站会这样裸奔吗?说实话,直接拼接用户输入并拼进 SQL 的情况,在老旧系统里还很常见;就算现在的框架普遍用 ORM,碰到动态排序、复杂报表、搜索等场景,手写 SQL 的概率仍然存在。

“隐藏数据”在真实业务里的形态也比题目里更丰富。比如:

  • 电商后台的未上架商品,字段status = 0;
  • 内容管理系统里的草稿文章,字段published_at IS NULL;
  • 用户体系里的内部测试账号,字段is_test = 1;
  • 金融系统里的历史记录,被deleted = 1软删除。

攻击者使用OR 1=1--这类注入,本质就是把“必须满足全部条件”的 WHERE 子句,改写成“只需要满足其中一个条件”的逻辑。原本的过滤条件全部失效,未发布、未审核、软删除的数据就会全部流到前端。

这里我必须多说一句,包括 PortSwigger 靶场在内的所有实验环境,都是经过授权的漏洞研究环境。现实中任何针对非授权目标的注入尝试都是违法的,所以这篇文章里的技术,请只放在你自己的靶场和授权测试范围内实践。

5.2 防御端最有效的三件套

这一关教会我们的核心千防御:别把用户输入当成 SQL 代码。

第一根防线是参数化查询。把用户输入当作数据传入,而不是拼接进语句。在 Python 里,正确和错误的写法差别非常直观:

# 错误示范:直接拼接 query = f"SELECT * FROM products WHERE category = '{category}' AND released = 1" cursor.execute(query) # 推荐做法:参数绑定 cursor.execute( "SELECT * FROM products WHERE category = ? AND released = 1", (category,) )

用参数绑定之后,即使用户输入再诡异的' OR 1=1--,数据库也只会把它当成一个普通的字符串值,不会介入 SQL 语法解析。

第二根防线是对输入做白名单校验。尤其是 category 这类业务枚举值,后端完全可以维护一个合法分类列表,只有命中的才允许进入查询。这样就算攻击者构造出再刁钻的 payload,也到不了 SQL 层。

第三根防线是数据库账号权限最小化。如果查询只读商品表的账号本身就不具备访问其他表的权限,攻击者想通过 UNION 注入拖取用户表、订单表,就会因为权限不足而失败。权限收得越紧,注入的破坏力就越小。

我建议做开发或运维的朋友,把这三层都当成默认配置。第一层防住大多数注入,第二层防住粗糙的参数污染,第三层防住注入成功后的横向影响。

5.3 做完这一题之后,下一步该练什么

PortSwigger 官方把 SQL 注入系列排成了好几关,第一题只是热身的开始。打完这一关,我建议按照下面的顺序继续:

  • 第二题:还是 WHERE 子句注入,但要先确认数据库列数,使用 UNION 检出其他表的数据。这能帮你理解注入不是只能绕过条件,还能“代查”其他数据。
  • 第三题:同样用 UNION 检索,但结果需要展示为登录账号密码。这离真实利用更近一步。
  • 登录绕过题:终于轮到“万能密码”场景,但你经历过这一关后,看到那个' OR '1'='1的感受会和最初完全不同。

把基础查询、注释符、URL 编码、条件构造这四个点吃透之后,你会发现后面几十道 SQL 注入题,本质都是在这些基础逻辑上加不同的“壳”。

写在小实验之外

我个人最深的体会是,这一关之所以被官方排在第一道,不是因为它的利用逻辑最简单,而是因为它把 SQL 注入最底层的那根弦拨响了:你对一个参数输入的内容,会如何改变一条 SQL 语句的结构。理解了这点之后,再去记各种 payload,就只是像背字典一样轻松了。

做完这道题,我建议你也像我一样,把它当成一个“最低标准”来审视自己写过的代码。随便打开一个旧项目,搜索一下有没有把请求参数直接拼到 SQL 字符串里的地方,如果有,趁早改成参数化查询。这道实验题救不了真实业务,但如果你愿意用这道题的角度去回看自己的代码,它会很值。

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

OpenClaw智能体框架从零部署实战:Ollama本地模型与技能开发全指南

“小龙虾”这个代号我在项目里用了大半年,一开始纯属随手起的,因为团队里讨论OpenClaw的时候总容易跟另一个内部项目搞混,就叫顺口了。但没想到这个名字在社区里慢慢传开,经常有人问“小龙虾是不是你们给OpenClaw起的官方中文名”…

作者头像 李华
网站建设 2026/10/16 12:42:10

AI知识库实战:从存好、管好到用好的完整指南

你搭过AI知识库吗?我最近连续帮几个团队做了知识库体检,得到的结论惊人地一致:文档传了几百份,权限也配了,Embedding也跑了,问AI助手的时候它还是答不上来,或者答得一本正经地错。这个现象太普遍…

作者头像 李华
网站建设 2026/10/19 13:29:49

基于合作博弈的综合能源系统多主体调度与利益分配Matlab实现

先说结论:这个项目核心就三件事,建模、调度、分钱。综合能源系统现在不是什么新鲜概念,但真正落地时最头疼的不是设备选型,也不是潮流计算,而是“多个利益主体怎么合作、合作之后怎么分钱”。单独运行的园区、楼宇、社…

作者头像 李华
网站建设 2026/10/15 12:48:26

基于Java的电子合同电子签名系统源码自研与多端接入实践

自打开始接电商、供应链这块的项目,合同这事儿就一直绕不开。以前图省事,直接用第三方电子签SaaS,按份数付费,一年下来账单挺吓人。后来接到一个客户需求,要求合同签署能力要嵌到他们的小程序、公众号、APP、H5里&…

作者头像 李华
网站建设 2026/10/20 8:21:38

网络时间同步技术演进:从NTP到PTP与TSN的精密时代

1. 为什么网络需要“对表”:一切问题的起点很多人刚接触网络的时间同步时,第一反应往往是:联机上个网而已,时间对不上能有多大影响?直到你第一次在分布式系统里看到两台服务器的日志时间差了整整几秒,排查了…

作者头像 李华
网站建设 2026/10/19 17:44:12

最新英语网络热词口语实战指南:从slay到rizz的地道用法

这种话题我本来不想写太长,但每次在交流群里看到有人把“slay”用成“slavery”的缩写,或者把“no cap”理解成“没戴帽子”,我就觉得必须好好聊聊了。作为一个每天都在刷英文社交媒体、跟外国同事开语音会、又在带英语口语训练营的人&#x…

作者头像 李华