这份实验报告其实拖了一个多月才整理完。起因是给某内部课程设计做安全测试系统本身是一个模拟电商平台测试范围明确所有操作都在本地虚拟机里完成。我分别验证了传统的 SQL 注入和基于 Cookie 的 SQL 注入前者是从登录框参数入手后者是把请求头里的 Cookie 值改掉后再拼进 SQL 查询。两个漏洞看起来入口不同但底层是同一个问题把不可信的用户输入直接拼进了数据库查询语句。这篇文章会完整记录环境搭建、注入原理、手工利用链路、踩坑修复过程适合正在学 Web 安全、想动手做点实验但又没有太多实操经验的人参考。1. 实验环境与靶场搭建思路1.1 环境选型为什么选虚拟机加开源靶场我试过直接在本地物理机装 LAMP 环境跑漏洞代码踩了几次坑数据库服务起冲突、系统环境变量污染、测到一半把系统配置改坏最后只能重装。所以这次学乖了先装一台虚拟机打快照再在虚拟机里布置靶场。虚拟机我用的是常见的开源虚拟化方案操作系统选了 Debian 系的精简版跑一个 PHP MySQL 的 Web 环境。虚拟机的网络模式设为仅主机Host-Only保证靶机和外网隔离实验过程不会影响其他网络。做漏洞实验最重要的前提是“靶机环境隔离”无论用什么工具、什么操作都不会对公网造成影响。靶场代码没有用我自己的业务系统而是挑了一个开源 SQL 注入练习平台再用 PHP 手工补了一个带 Cookie 识别逻辑的模块。之所以用开源靶场做底子是因为它自动带了多个安全级别的开关能方便地模拟低防护环境。我又在这个基础上改了一个“模拟电商订单查询页”登录成功后把用户名放进 Cookie后端用 Cookie 值去查询订单这就天然形成了一个 Cookie 注入点。整个过程全部是本地模拟数据不涉及任何真实用户信息。实验环境清单如下虚拟机4GB 内存双核 CPU磁盘快照开启。操作系统Linux 精简版。Web 服务Apache PHP 7.x。数据库MySQL 5.7。靶场代码某开源 SQL 注入练习平台 自写 Cookie 订单模块。测试工具浏览器开发者工具、抓包修改请求的代理工具、Python 脚本下单的请求库。1.2 靶场场景设计一个带 Cookie 识别的购物车模块模拟系统是一个小电商主要流程是用户登录服务端校验用户名密码登录成功后把username写入 Cookie后续用户访问“我的订单”页面服务端直接从 Cookie 里取出username然后用它拼接 SQL 查询。我故意设计了三张表users存放模拟用户名、密码哈希、邮箱。products存放模拟商品。orders存放模拟订单记录字段包含订单号、用户 id、金额、创建时间。数据库初始化时往orders表里灌了几十条模拟订单分别挂在两个模拟用户下面。这样测试时可以看到“有数据和没数据”的页面差异方便判断注入条件是否成立。这个场景选得比较有意思因为表单登录处的 SQL 注入大家练得很多但 Cookie 里的值通常被看成“已经由服务端生成的可信数据”新手很容易忽略。实际上 Cookie 存储在客户端客户端就能被任意修改。服务端一旦把 Cookie 值直接拼进 SQL就等价于把数据库查询暴露给了用户输入这一类漏洞在真实项目里也出现过只是很多测试报告没有单独列出来。2. 两个关键漏洞的原理拆解2.1 SQL 注入本质查询语句“拼接”了一个可输入的开关SQL 注入的根因不是数据库的问题而是开发者在编写查询语句时直接把用户输入嵌进了 SQL 文本。SQL 是一种解释型语言服务端把整段字符串交给数据库执行。如果中间掺入了用户可控内容数据库会把它当成 SQL 指令的一部分而不是单纯的数据。可以用生活类比SQL 查询像是一张固定格式的请假单“姓名”栏填的内容最终会被打印进表格里。现在表格模板是“请让 XXX 在 YYYY 日期请假”如果有人在“姓名”栏填了“张三”并附带一句“且让我审批全部流程”打印出来以后整张单据的意思就变了。SQL 注入就是这种“输入内容改变了原语句逻辑”的问题。举个我在实验里复现的代码例子登录模块如果这么写$username $_POST[username]; $password $_POST[password]; $sql SELECT * FROM users WHERE username . $username . AND password . $password . ;正常提交usernamealicepassword123456SQL 就是SELECT * FROM users WHERE username alice AND password 123456但如果在用户名字段提交alice -- -SQL 变成SELECT * FROM users WHERE username alice -- - AND password 123456-- -是 SQL 的注释符号注释符后面的内容全部被忽略。整个查询条件就只剩usernamealice密码验证形同虚设。这就是注入最直观的破坏力通过闭合原有引号再借助注释符吞掉后半段逻辑从而绕过校验。字符型注入比数字型注入多一个“闭合”的步骤。数字型查询如WHERE id 1直接输入1 OR 11就能改变条件字符型查询如WHERE username alice需要先输入一个单引号把字符串闭合再追加自己的 SQL 片段最后还要用注释符把末尾可能存在的引号处理掉。2.2 Cookie 注入入口换成了 Cookie问题依然是参数化Cookie 是浏览器存储在客户端的小段文本每次请求都会自动携带到服务端。服务端靠它识别会话、保存偏好、记录身份。很多开发者在写业务代码时会为了图方便直接把 Cookie 里的值拿来做查询条件。我在靶场里加的这个订单模块就是典型代码$userId $_COOKIE[user]; $sql SELECT * FROM orders WHERE uid $userId;从代码看$_COOKIE[user]是后端从请求头里读出来的值但它并不比$_POST[username]更可信因为 Cookie 内容可以被用户随时修改。比如我用浏览器开发者工具把 Cookie 里的user值改成user alice OR 11最终 SQL 变成SELECT * FROM orders WHERE uid alice OR 11这里OR 11永远为真于是整张订单表的数据全部被查了出来。注入效果等价于把整个数据库表的模拟订单“拖”下来只是入口从表单参数换成了 Cookie 头。为什么 Cookie 注入会让人忽视因为测试 SQL 注入时大部分人习惯盯着 GET 参数和 POST 参数却忘了 HTTP 请求的每个部分都可能被攻击者控制。Cookie 特殊的地方在于它由服务端在响应头里下发并在客户端保存但这并不代表它是服务端私有数据。客户端能看见、能修改任何从客户端带回来的值都要当成不可信输入。2.3 两者的异同与联动关系传统 SQL 注入和 Cookie 注入本质上是一家人只是“入口”不同。我整理了一个简单的对比表对比维度表单 SQL 注入Cookie SQL 注入输入位置GET/POST 参数HTTP 请求头 Cookie复现难度直接在浏览器改 URL 或表单需借助抓包工具或浏览器修改 Cookie 的插件防护盲区常规 WAF 和参数校验会重点盯很多 WAF 默认不深度检测 Cookie 内容危害程度数据库被脱库、登录被绕过同样能拖库、越权还可能造成“会话身份伪造”修复方式参数化查询、白名单校验参数化查询、Cookie 仅存随机会话标识两个漏洞还有一个联动场景Cookie 注入往往可以作为 SQL 注入的“二次利用入口”。比如表单登录处已经加了参数化查询但登录成功后服务端把用户名明文写进了 Cookie后续某个业务接口读 Cookie 再拼 SQL攻击者只需要改 Cookie就可以绕过前端表单限制直达后端数据库查询。这也说明修复不能只堵一个点要把所有输入端都纳入审查范围。3. 实验实操从手工注入到完整利用3.1 第一步信息收集与注入点定位靶场环境启动后我首先打开模拟系统首页正常登录一个实验账号。登录成功后访问“我的订单”页面页面显示了该用户下的订单列表同时在浏览器开发者工具里能看到服务端返回了Set-Cookie: useralice这样的响应头。我把这一条请求记录下来作为后续比较用的“基线请求”。传统登录框的注入点定位很简单先输入一个单引号提交比如用户名填alice如果页面报错或者返回异常说明存在 SQL 拼接的可能。另一种更稳妥的方法是输入两条看起来相同的逻辑条件然后对比页面差异提交alice AND 11页面正常显示订单提交alice AND 12页面无数据或报错。这两条语句本质上都是usernamealice只是附加条件一真一假。如果页面表现不同就可以判断后端的 SQL 语句里确实拼接了用户输入且闭合方式和我猜的一致。Cookie 注入点的定位思路完全相同只是操作位置不同。我用抓包工具拦截到“我的订单”请求把 Cookie 里的useralice临时改成useralice看看服务端是否返回异常。实际测试中我第一次改完后页面直接抛出数据库语法错误这就说明 Cookie 值被代入 SQL 了。为了确认不是误报我又把 Cookie 改成useralice AND 11页面恢复正常改成useralice AND 12页面恢复异常逻辑闭环确认注入存在。3.2 第二步SQL 注入完整利用链路联合查询确认注入点后需要判断查询列数。联合查询UNION SELECT要求前后两个查询列数一致所以先要确定原查询一共选了哪几列。在登录框注入时我用的是ORDER BY试探法。逐个增加排序序号alice ORDER BY 1 -- - alice ORDER BY 2 -- - alice ORDER BY 3 -- -如果ORDER BY 3时页面正常ORDER BY 4时页面报错说明原查询有 3 列。为什么用ORDER BY因为排序超过查询列数时数据库会报“order by 项必须出现在选择列表中”的错误这个报错能精确反映列数上限。确定列数是 3 后开始找回显位置alice UNION SELECT 1,2,3 -- -如果页面里出现数字2和3说明第 2 列和第 3 列能直接输出到页面。接下来就可以把这些位置替换成真正想查询的数据。先查当前数据库名alice UNION SELECT 1,database(),3 -- -页面第 2 列输出了类似shopdb的数据库名。然后查表名alice UNION SELECT 1,GROUP_CONCAT(table_name),3 FROM information_schema.tables WHERE table_schemadatabase() -- -information_schema是 MySQL 内部存储数据库元数据的系统库里面的tables表记录着每个数据库下有哪些表。利用它可以直接枚举整个数据库的所有表名。继续查询users表里的用户名和密码哈希alice UNION SELECT 1,GROUP_CONCAT(username,:,password),3 FROM users -- -到这里传统注入的攻击链已经走完拿到库名、表名、字段数据。整个过程中最需要耐心的是闭合引号和注释符的排列组合。字符型注入常见的闭合方式有、、)、)遇到二次注入和宽字节编码时还会更复杂。3.3 第三步Cookie 注入的完整链路Cookie 注入的实操流程和传统注入差不多但多了“先取得合法 Cookie”这一步。我先在浏览器里用实验账号正常登录拿到一个合法的会话 Cookie。然后用抓包工具抓取“我的订单”请求把 Cookie 里的user字段值替换成注入语句。这里不建议用浏览器自带的开发者工具直接修改 Cookie因为某些场景下浏览器会自动编码或转义部分字符数据传出去就不是你看到的原始值了。更稳妥的办法是用抓包代理工具拦截请求改完后放行这样看到的就是由工具直接构造的原始 HTTP 报文。确认 Cookie 注入点的方式和表单注入相同。我先把 Cookie 改成useralice AND 11页面正常显示订单数据然后改成useralice AND 12页面不再显示任何订单。这两个结果足够说明注入成立。继续枚举列数时由于页面展示的是订单列表不同列在页面上的渲染位置可能被表格隐藏了所以需要用UNION SELECT 1,2,3配合查看页面源代码确定隐藏的回显列。Cookie 注入和表单注入最大的区别在于“攻击入口的隐蔽性”。某些安全防护机制只检查 GET/POST 参数对 Cookie 内容的检查非常宽松。不过这并不代表 Cookie 注入“厉害到无法防御”本质上还是代码没用参数化查询。我在实验里看到的现象是修改 Cookie 不需要经过前端页面的表单校验逻辑等于直接和服务器对话这反而比处理 URL 参数更直观。3.4 参数计算与判断逻辑细节在做布尔盲注时由于页面没有直接回显数据我需要通过“页面是否显示订单记录”来判断 SQL 条件的真假。这种方法叫布尔盲注本质上是一个二进制判断过程条件为真页面正常条件为假页面异常。比如我想知道当前数据库名的第一个字符是什么可以用alice AND ASCII(SUBSTRING(database(),1,1))100 -- -如果页面正常说明数据库名的第一个字符的 ASCII 码大于 100再改成大于 110继续缩小范围。通过二分法逐位猜测最终能拼出完整库名。我在实验里写了一段 Python 脚本用请求库自动发请求把需要判断的条件动态构造进去根据响应状态决定下一步。脚本逻辑很简单但加快了整个猜测过程。还有一个容易踩坑的地方逐位猜测时如果原始查询本身就返回多条记录页面显示可能没有明显差别。解决方法是先在注入语句中构造一个“必然为假”的原条件比如alice AND 10 -- -再在后面通过联合查询或条件分支来控制页面显示。这样能让注入结果表现得像开关一样清晰不至于因为多行数据导致判断失误。4. 常见问题与排查技巧实录4.1 为什么加了单引号页面却没有任何变化我在实验初期遇到过几次“输入单引号后页面照常显示”的情况。排查后发现原因有很多最常见的是三种服务端开启了错误显示屏蔽数据库报错信息没有输出到页面开发代码把输入做了转义处理单引号被加上了反斜杠闭合方式不是直接的单引号可能用了)或)这种嵌套括号的拼接方式。解决思路不是只靠报错而是改用“逻辑差异法”。提交AND 11和AND 12两条完全同构的语句观察页面表现是否不同。如果两条都有差异说明注入判断条件成立如果两条都没差异可能是注释符没正确闭合或者当前注入点不在这个参数上。另外要注意--注释符在 MySQL 后面必须跟一个空格写成-- -是最保险的。有些数据库不支持--注释要改用#这取决于数据库类型和版本。4.2 改 Cookie 后页面异常但不知道注入点成不成立Cookie 注入时页面变化有时候不够直观。因为 Cookie 里的特殊字符会被 HTTP 头格式影响比如分号、空格都有特殊含义。直接修改浏览器 Cookie 时工具可能把单引号做了一次 URL 编码服务端拿到的就不是原始单引号。我的建议是把抓包工具当第一选择。工具里能看到完整的原始请求头修改 Cookie 后放行服务端收到的内容和工具显示的完全一致。如果只能用手工方式也要对 Cookie 值做 URL 解码后再观察。另一个技巧是使用时间盲注来确认条件。在 Cookie 值里插入alice AND SLEEP(5) -- -如果服务端响应时间显著增加到 5 秒左右就说明 SQL 语句确实执行了SLEEPCookie 注入点必然存在。时间盲注的适用场景是页面完全没有回显和报错但依然能通过响应时间判断数据库是否执行了注入指令。4.3 修复后漏洞还在问题出在哪实验做了一半我顺手尝试给靶场代码打补丁。一开始用的是“黑名单过滤”把select、union、等关键词替换成空字符串。结果发现大小写绕过太容易输入SeLeCt就能绕过后来改成正则匹配结果宽字符编码绕过又出现了。这让我意识到一个核心问题靠过滤关键词来修复 SQL 注入永远是“你遮左脸他就打右脸”。正确的修法是参数化查询把 SQL 模板和用户数据彻底分离。黑名单过滤只能作为临时缓解措施不能当最终方案。我还在实验里发现了另一个隐蔽问题即使登录查询用了参数化后面订单查询如果继续用字符串拼接漏洞依然存在。所以修复要覆盖所有 SQL 执行点不能只修登录模块。审计时应该排查所有出现$_GET、$_POST、$_COOKIE、$_REQUEST的地方看哪些值被直接拼到了 SQL 语句里。5. 防御加固与后续延伸5.1 根本修复参数化查询与预编译语句无论注入出现在表单还是 Cookie最直接的根治办法是改用预编译语句。以 PHP PDO 为例$stmt $pdo-prepare(SELECT * FROM orders WHERE uid ?); $stmt-execute([$_COOKIE[user]]); $orders $stmt-fetchAll();核心变化是SQL 模板WHERE uid ?先交给数据库编译之后再把$_COOKIE[user]当作纯数据传进去。SQL 的语法结构已经固定用户输入永远不能改变语句逻辑。Java 的写法类似String sql SELECT * FROM orders WHERE uid ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, request.getCookies() 中读到的 user 值); ResultSet rs ps.executeQuery();只要代码库里统一使用预编译SQL 注入的根因就被消除了。我在实验里把登录模块、订单模块都改成了参数化查询后重新跑前面所有注入攻击没有一种能生效。5.2 Cookie 的专项防护Cookie 注入的另一个修复点是“服务端不要拿 Cookie 内容直接作为业务身份凭证”。更安全的做法是Cookie 里只保存一段随机 session id用户身份信息放在服务端 session 里。用户打开订单页时服务端先用 session id 找到当前用户标识再用这个标识去查询订单。同时还要配置 Cookie 的安全属性HttpOnly禁止 JavaScript 读取 Cookie降低 XSS 窃取风险。Secure仅通过 HTTPS 传输防止明文传输中被截获。SameSite限制跨站请求携带 Cookie缓解 CSRF 问题。此外服务端日志里也要增加对异常 Cookie 的监控。我在实验日志里观察到注入攻击时 Cookie 值会出现连续单引号、union、select 等异常特征这类请求如果能实时告警即使 WAF 没有拦截安全人员也能及时介入。5.3 Web 应用防火墙之外还需要什么有些系统会加 WAF 来拦截注入请求。WAF 可以拦截一部分自动化攻击但它不是万能的。漏报、误报、绕过问题始终存在。我在实验里尝试过对 Cookie 注入加入简单 WAF 规则确实能挡掉部分关键字但只要攻击者变换编码方式、注释符、空白字符规则又可能需要调整。所以推荐的做法是“纵深防御”预编译是根本输入校验是辅助WAF 是兜底日志监控是持续发现。代码评审阶段就要明确一个原则——任何客户端可控数据包括 Cookie都不能直接拼进 SQL。这个原则如果贯彻到底SQL 注入相关漏洞基本可以绝迹。6. 实验复盘与个人心得整个实验做完以后我对“漏洞利用”这件事的体感变化很大。以前总觉得 SQL 注入就是“在输入框里加个单引号”真正亲自动手走完整条链才发现很多细节只有踩过坑才会在意闭合方式判断、回显位置定位、注释符写法、Cookie 编码问题任何一个环节出错都可能导致测试中断。Cookie 注入给我的启发是安全测试的视野不能局限于表单参数。只要请求里能被客户端篡改的信息都可能是注入入口。不只是 Cookie包括请求头里的User-Agent、Referer、X-Forwarded-For都有可能在服务端代码里被拼接进 SQL。测试时不能只看“界面有什么输入框”还要看“服务端到底读了哪些数据”。给同样在练习的朋友三个建议第一所有实验必须在授权的、隔离的靶场环境中进行绝对不要在公网未授权系统上操作第二手工注入至少完整做一遍完全依赖工具会掩盖很多对原理的理解第三每次攻击成功后回头想一想对应的修复方案安全测试的一半价值在漏洞本身另一半价值在知道怎么彻底堵住它。这次实验之后我又把整套靶场扩成了包含报错注入、布尔盲注、时间盲注和编码绕过的综合练习环境。每次做完一轮测试都会重新初始化数据库保证数据干净可复现。如果你也想研究 Web 漏洞建议先从 SQL 注入入手它足够体系化能帮助你把请求、响应、数据库、服务端代码这几个关键环节串起来。
阅读完成 · 觉得有帮助?