1. 前言:为什么Cookie会成为攻击者的突破口在做Web安全测试时,很多人习惯性地只关注URL参数和表单提交的数据,却忽略了一个非常关键的输入点——Cookie。Cookie注入攻击的原理其实并不复杂,但它的隐蔽性极强,很多开发者在做安全防护时,会把SQL注入的过滤逻辑写得很完善,却唯独漏掉了Cookie这个入口。结果就是应用的前端功能稳如泰山,攻击者却在Cookie上轻松撕开了一道口子。这篇文章是《Web安全原理剖析》系列的第九篇,重点讨论Cookie注入攻击的完整链路。我会从Cookie的本质上手,拆解它为什么会成为注入的高危入口,然后结合真实的测试场景,展示一个典型的Cookie注入攻击是如何从识别、利用到提取数据的全过程,最后给出对应的防御方案。无论你是做安全测试、渗透测试,还是负责应用开发和运维,弄清楚这条攻击路径都非常有必要——因为你大概率正在维护的某个系统,Cookie头可能就是一个不设防的后门。2. 深入理解Cookie注入的本质在讲具体的攻击手法之前,必须先理清一个问题:Cookie和SQL之间到底存在什么关系,为什么一个看似无害的小文本文件能够成为攻击者注入SQL语句的载体?2.1 无状态协议与Cookie的诞生HTTP协议设计之初就是无状态的,这意味着服务器无法区分两次独立的请求是否来自同一个用户。打个比方,你去一家自习室学习,管理员第一次见到你,登记了你的名字,但第二天他完全不记得你是谁了——你需要重新登记一遍。这显然很低效。于是Cookie被设计出来,用来让服务器“记住”用户。当用户第一次访问站点时,服务器会在响应头里写入Set-Cookie,浏览器将其保存在本地。后续的每一次请求,浏览器都会自动携带这个Cookie,服务器拿到Cookie后就可以判断当前请求的身份状态。这种机制本身没有问题,但问题在于:服务器接收到的Cookie数据,本质上和你在地址栏输入的参数没有任何区别——它同样是一条由客户端控制的数据流。2.2 SQL注入对输入来源的宽容性我们熟悉的URL参数注入、POST表单注入,本质上都是因为开发者把用户的输入未经严格过滤就拼接进了SQL语句。比如一个登录功能:$sql = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "'";这里存在典型的SQL注入。但如果开发者把同样的逻辑用在Cookie数据上:$sql = "SELECT * FROM users WHERE user_id = '" . $_COOKIE['user_id'] . "'";问题就会完全一样地出现在Cookie上。Cookie注入的本质就是:开发者默认Cookie数据是“安全”的,认为只有服务器自己写入的内容才存在Cookie里,客户端无法篡改。这是一个致命误解——Cookie完全由客户端掌控,任何攻击者都可以手工构造一个包含恶意SQL语句的Cookie值,然后在请求中发送给服务器。2.3 从攻击面视角看Cookie注入的价值从攻击者的视角看,Cookie注入有几个非常诱人的特点:隐蔽性高:很多WAF(Web应用防火墙)会重点检查GET和POST参数,对Cookie的检查相对宽松,甚至完全忽略。命中率高:很多开发者对GET参数做了过滤,但使用Cookie读取的参数往往直接在SQL语句中拼接。自动化绕过:传统的SQL注入扫描器很多不会主动发送恶意Cookie,导致自动化工具扫不出漏洞,但人工测试可以精确命中。可绕过登录限制:很多应用的Cookie中存储了用户身份标识(如ID),如果注入点在身份标识上,攻击者甚至可以越权提取其他用户的数据。这些特点决定了Cookie注入在真实攻击中的利用率极高。我做过不少授权范围的安全评估项目,Cookie注入出现的频率远超大多数人的想象,而且因为隐蔽性强,往往会存在很久才被发现。3. Cookie注入的分类与核心场景Cookie注入的SQL语句形态和普通SQL注入是一样的,只是入口不同。常见的Cookie注入主要有以下几种典型场景。3.1 整数型Cookie注入这是最简单、也最容易出漏洞的形式。典型代码如下:$userId = $_COOKIE['user_id']; $sql = "SELECT * FROM users WHERE user_id = " . $userId;如果用户在Cookie中设置user_id = 1 OR 1=1 --,那拼接出来的SQL就变成了:SELECT * FROM users WHERE user_id = 1 OR 1=1 --这里--是SQL注释符,后面的内容全部被注释掉。这个语句会把users表的所有记录全部查询出来。如果应用在登录状态判断时使用了这个查询结果,就可能直接绕过验证。3.2 字符型Cookie注入另一种常见的形式是在字符串拼接场景下。典型代码如下:$username = $_COOKIE['username']; $sql = "SELECT * FROM users WHERE username = '" . $username . "'";攻击者在Cookie中填入admin' --,拼出来的SQL就变成:SELECT * FROM users WHERE username = 'admin' -- '这个攻击方式是直接以管理员的身份登录。我见过不少后台管理系统用Cookie存储用户名,而开发者在拼接用户名时没有做任何转义,导致一个简单的请求头就把整个后台大门敞开。3.3 搜索、筛选与排序场景还有一种容易被忽略的场景是数据列表页。很多列表页会把搜索关键字、排序字段、分页编号保存在Cookie中,方便用户下次访问时记住上次的状态。比如某个后台订单列表,它会读取Cookie中的orderBy字段来做排序:$orderField = $_COOKIE['order_field']; $sql = "SELECT * FROM orders ORDER BY " . $orderField;这种情况下,攻击者可以把order_field设置为1; DROP TABLE orders --,虽然这种堆叠注入在部分数据库方言下无法执行,但每一句恶意排序字段都可能造成信息泄露或数据损坏。4. 手工检测Cookie注入的完整思路在实际测试中,要判断Cookie是否存在注入,需要遵循一套严谨的流程。按下述步骤操作,绝大多数隐蔽的Cookie注入点都会现出原形。4.1 环境准备与授权确认如果你是安全测试人员或学习者,请务必在自己的测试环境,或者获得授权的测试环境中进行。我一般会搭建一个本地靶场环境,或者使用现成的漏洞靶场来练习。这一步的目的是确保攻击行为完全合法合规。工具方面,你只需要一个浏览器和一个能够自由修改请求的工具。浏览器自带的开发者工具能看请求响应,而修改Cookie则需要借助一些浏览器扩展,或者使用功能更完整的HTTP请求工具(如Burp Suite)。4.2 收集站点Cookie结构第一步是正常访问应用,打开开发者工具,观察请求头中的Cookie字段。注意每个Cookie的名称、数量和取值规律。重点关注的Cookie类型包括:user_id、uid、userId这类身份标识型Cookieusername、uname这类用户名型Cookietoken、session、auth这类认证信息型Cookielang、theme、page、order这类功能和偏好型Cookie对于上面每一种Cookie,初步判断它是否会被用作SQL语句的拼接数据。Cookie名称本身就是一个提示信号,比如cat_id这种,几乎可以肯定它会关联数据库查询。4.3 单引号探测法对每一个可疑Cookie,用单引号替换原有值,观察应用响应是否有异常。以cat_id=2为例:原始请求: Cookie: cat_id=2 探测请求: Cookie: cat_id=2'如果cat_id直接拼接进SQL,数据库会报语法错误,页面可能出现数据库错误信息,也可能出现500状态,或者服务器直接返回一个空白页。无论哪种异常,都值得进一步验证。我见过一种特殊情况:页面不报错,但返回了正常的空内容。这种情况往往是因为SQL语法错误被应用层吞掉了,数据库查询失败,代码没有处理就继续执行。这种“平静的异常”同样需要高度重视,要结合时间延迟等方式来进一步判断。4.4 逻辑判断探测法单引号报错之后,接着用逻辑表达式来验证。对于一个整数型注入点,把Cookie值改为2 AND 1=1,页面应正常;再改为2 AND 1=2,页面应异常。两次响应有差异,就能确定这里的Cookie值被拼进了SQL。对于字符型注入点,先构造2' AND '1'='1和2' AND '1'='2来做对比测试。注意字符型注入在拼接时需要处理好引号的配平关系。4.5 时间盲注探测法如果页面不显示任何错误信息,也没有逻辑差异,可以试试时间盲注。把Cookie值改成:2 AND SLEEP(5) --(MySQL写法)2 AND pg_sleep(5) --(PostgreSQL写法)如果页面响应时间明显延迟了大约5秒,说明语句被成功执行,这里可以确定存在注入点,而且使用的是时间盲注方式。时间盲注在实际测试中非常有用,但要注意控制单个探测点的延时时长,避免产生大量请求导致日志异常或服务压力。一般我会用3秒或者5秒,既能明显感知,又不至于拖垮目标。5. 从识别到利用的完整实例拆解判断出注入点之后,接下来就是利用的过程。这里我用一个典型场景来走一遍完整流程,让你直观地看到Cookie注入能造成多大的危害。5.1 场景设定与初步判定假设某个内容管理系统(CMS),用户登录后,系统在Cookie中写入了一个userType字段。普通用户的默认值是1,管理员的值是0。系统后台代码中有如下逻辑:$userType = $_COOKIE['userType']; $sql = "SELECT * FROM content WHERE user_type = " . $userType . " AND status = 1";这个SQL在应用中的作用是:根据用户的类型,加载其有权限查看的内容。我们先用单引号探测:Cookie: userType=1'页面返回数据库异常。再用逻辑判断:Cookie: userType=1 AND 1=1 页面正常,内容正常加载。 Cookie: userType=1 AND 1=2 页面空内容,没有显示任何数据。这就能确认:Cookie中的userType直接拼入了SQL语句,且是整数型注入。5.2 判断字段数量并完成联合查询要用联合查询提取数据,需要先知道查询字段的数量。用普通的ORDER BY探测法就能搞定:Cookie: userType=1 ORDER BY 1 -- 正常 Cookie: userType=1 ORDER BY 5 -- 正常 Cookie: userType=1 ORDER BY 10 -- 报错通过逐级递增测试,确定查询共有9个字段。然后构造联合查询:Cookie: userType=1 UNION SELECT 1,2,3,4,5,6,7,8,9 --页面显示了内容位置2、4、7上的数字,说明这3个字段会被输出到页面上。于是把对应位置替换成我们需要的数据:Cookie: userType=1 UNION SELECT 1,version(),3,database(),5,6,user(),8,9 --实际执行后,页面回显了数据库版本、当前数据库名、数据库用户等信息。到这里,一个基本的Cookie注入点已经可以被完整利用了。5.3 提取核心敏感数据因为注入点在CMS的内容查询上,我们可以进一步读取其中的管理员表。先猜测表名为admin_users,字段名为username和password:Cookie: userType=1 UNION SELECT 1,username,3,password,5,6,group_concat(username,0x3a,password),8,9 FROM admin_users --这里的0x3a是冒号:的十六进制,用于在输出时把用户名和密码拼接成可读的一行。如果管理员密码是明文存储,这里会直接暴露明文。如果密码是哈希,攻击者后续可以离线破解。这一步做完,你对Cookie注入的威胁程度就有了清晰的认知:一个排除了所有前端过滤的Cookie值,直接把后端数据库的表结构和核心数据全部暴露给攻击者。6. 不同数据库方言下的Cookie注入差异Cookie注入利用手法的实现,高度依赖于后端使用的数据库类型。不同数据库的语法方言、注释方式和字符串拼接规则都有所不同,实际测试时需要区别对待。6.1 MySQL / MariaDB这是国内Web应用使用最广泛的数据库组合。常用特性:注释符支持#、--(注意--后面必须跟一个空格)和/*...*/信息获取函数:version()、database()、user()、@@version_compile_os字符串拼接函数:CONCAT()、CONCAT_WS()延时函数:SLEEP(n)、BENCHMARK(n, expr)MySQL还有一个非常有用的能力——在UNION SELECT中直接使用GROUP_CONCAT()把多行数据合并成一行,方便读取。我在上面的实例中就用了这个方法。6.2 Oracle数据库Oracle在大型企业系统中仍然占有相当大的份额。Oracle和MySQL的SQL方言差异很大:Oracle没有LIMIT,使用的是ROWNUM,且ROWNUM的取值逻辑和LIMIT完全不同。注释符只支持--,不支持#。表名、字段名查询需要从系统视图ALL_TABLES、ALL_TAB_COLUMNS中读取。字符串拼接用的是||操作符,比如SELECT 'a' || 'b' FROM dual。延时函数可以使用DBMS_LOCK.SLEEP(n),但这个函数需要合适的权限才能执行。在Oracle场景下,Cookie注入的利用流程会更复杂一些,尤其是数据提取需要依赖系统视图,SQL语句会变得很长,手工测试的效率会明显降低。6.3 SQL ServerSQL Server主要出现在Windows系统架构中。它有几个特殊的地方:注释符支持--和/*...*/。系统函数DB_NAME()返回当前数据库名,@@VERSION返回版本信息。分号堆叠执行的限制相对较少,在特定条件下可以执行多条语句。SQL Server的字符串拼接用+操作符。延时参考使用WAITFOR DELAY '0:0:5'。SQL Server场景下有一种经典的利用方式:如果后端是ASP或.NET应用,且数据库连接权限较高,攻击者甚至可以通过注入开启xp_cmdshell来执行系统命令。虽然大多数现代配置会禁用该组件,但如果运维人员图省事保留了默认权限,这种攻击会导致严重的系统失陷。这里我仅仅是提示这种可能性,读者务必清楚这是一条高危链路,只在授权环境下测试。6.4 SQLiteSQLite常见于小型应用、嵌入式场景和部分桌面级Web应用。它的SQL语法接近MySQL,但有一些限制:没有version()函数,但可以通过sqlite_version()获取版本。不支持MySQL风格的UNION SELECT字符串拼接函数(没有CONCAT()),但可以用||来实现拼接。系统表是sqlite_master,用来存储表结构信息。注释符支持--和/*...*/。SQLite注入的利用会受限于它的数据类型,因为它的列类型是弱约束的,有时候联合查询的数据类型匹配更加宽松,反而更容易构造Payload。7. 绕过过滤与WAF的进阶手法如果目标系统对Cookie输入做了些过滤,或者部署了WAF,基础注入Payload会被拦截。这时候就需要对Payload进行变形,以提高绕过概率。7.1 大小写、注释与编码变形当过滤规则是关键字匹配时,大小写混写经常能直接绕过:union select → UnIoN SeLeCt如果大小写也被过滤,可以用内联注释分割关键字:un/**/ion sel/**/ectMySQL支持在关键字中间插入注释,注释部分会被忽略,但拼接后的SQL语句不受影响。URL编码是另一个方向。有些WAF对GET参数的解码很积极,对Cookie参数的解码则不够全面。可以把Payload中的部分字符做URL编码,看后端能否正确解码。我遇到过一些用ASP.NET搭建的系统,Cookie中传入URL编码后的单引号%27,后端框架会自动解码,但WAF层却绕过去了。7.2 等价替换函数很多过滤规则是针对特定函数名的,比如封禁sleep、version、database。这时可以用功能等价的替代方案:用SUBSTR()替代MID(),或者反过来。用BENCHMARK()替代sleep()做延时。用@@datadir、@@hostname这类系统变量替代函数获取信息。用十六进制表示字符串,绕过针对引号的过滤,比如0x75736572表示user字符串。需要说明的是,绕过WAF是一个动态博弈过程,没有包打天下的Payload。关键在于理解WAF的解析逻辑和后端数据库的解析逻辑之间的差异,利用两者的不对称性来寻找绕过点。7.3 利用注释符截断后续语句字符型注入时,Payload结尾经常要用注释符把后面的引号和SQL语句截断掉。不同数据库的注释符不兼容,这也是一个容易被忽略的细节。比如在MySQL下用#,在Oracle下就必须换成--,少一个空格都可能导致语法错误。很多新手在做跨数据库测试时,经常因为注释符问题白白浪费时间,排查半天才发现是注释符不匹配。8. 防御方案设计:从源头封堵Cookie注入防守方的思路其实不复杂:把攻击的所有入口都堵上,重点把握好以下三个环节。8.1 参数化查询是根治手段防御SQL注入的根本手段是使用参数化查询(Prepared Statement),让SQL语句的结构和参数数据完全分离。以下以PHP的PDO为例:$stmt = $pdo->prepare("SELECT * FROM content WHERE user_type = ? AND status = 1"); $stmt->execute([$_COOKIE['userType']]);参数化查询会确保传入的值只作为数据处理,永远不会被当作SQL代码解析。无论攻击者在Cookie中放什么内容,都会被当作普通字符串传给数据库。对于Java开发者,可以使用PreparedStatement;Python开发者可以使用数据库驱动自带的参数绑定;.Net开发者则使用SqlCommand的参数集合。所有主流语言和框架都提供了参数化查询的支持,关键是要养成习惯,凡是输入数据一律走参数化。8.2 输入校验与白名单机制有些场景不宜直接套用参数化(比如动态排序字段),这时候必须用白名单校验逻辑。比如排序字段,不可能靠参数化解决问题,因为字段名不应当是用户自由输入的内容,而应当交给白名单:$allowedOrderFields = ['create_time', 'order_amount', 'order_status']; if (in_array($_COOKIE['order_field'], $allowedOrderFields, true)) { $orderField = $_COOKIE['order_field']; } else { $orderField = 'create_time'; }这里使用严格白名单,从源头上消除了恶意字段名的可能性。对于数字型数据,可以用is_numeric()或者强制类型转换。8.3 安全配置与纵深防御除了代码层面的修复,运行环境的安全配置同样重要:数据库账户权限最小化:连接数据库的应用账户,只授予其业务需要的SELECT、INSERT、UPDATE、DELETE权限,绝不授予DDL权限(如DROP、ALTER),更不授予FILE权限。这样即使注入成功,攻击者能造成的破坏也极其有限。关闭数据库错误回显:生产环境不要向用户展示SQL错误详情。WAF规则优化:确保Cookie字段也在检测范围内,且要对URL编码、混合编码做解码分析。定期安全扫描:不要只扫描URL参数,还要主动构造异常的Cookie请求做模糊测试。很多时候我发现系统出现SQL注入,不是由某个“高级漏洞”导致的,而是多个基础安全配置疏忽叠加的结果。如果把上面的每一项都做到位,这个站点的整体安全水位会立刻提升一个档次。9. 常见问题与排查技巧实录做Cookie注入测试和修复的这些年里,有一些高频问题反复出现。这里总结成一个速查对照表,方便你平时排查。9.1 常见问题对照表问题现象可能原因排查方向页面返回500,但是日志无SQL错误应用层主动吞掉了数据库异常开启数据库错误日志,或使用时间盲注验证单引号测试无反应输入被框架转义或过滤尝试二次编码或等价替换函数绕过联合查询字段数对,但页面无回显回显位不在当前页面模板中换一种输出方式,或改用布尔盲注延时注入无延迟Cookie值未到达SQL拼接点检查Cookie值是否被服务端修改或丢弃始终返回相同页面WAF对Cookie做静态检测尝试编码、注释、大小写混写绕过手工测试正常,扫描器测不出扫描器默认不测试Cookie人工测试始终是最可靠的手段9.2 容易被忽略的测试陷阱Cookie在传输过程中可能被多个中间环节修改,这会直接影响注入测试的稳定性和准确率。比如有些负载均衡器或反向代理会在请求转发时自动清理部分Cookie;有些框架会对Cookie做签名校验,Cookie值被修改后直接被丢弃,导致检测结果失真。还有一种情况值得注意:Cookie中可能保存了多个属性,比如Path、Domain、HttpOnly、Secure等。这些属性只影响浏览器行为,不会影响服务器端对Cookie值的处理。但测试时如果你用脚本直接修改Cookie值,一定要确保修改后的请求仍然背着完整的Cookie字段,不要因为格式问题导致请求被服务器拒收。另外,修改Cookie时要注意处理器的细节。以Burp Suite为例,它在History面板中可以直接右键修改请求并重放。修改后的Cookie会完整保存下来,方便快速迭代测试。这个工具链是我每次做Cookie注入测试时的标配,效率比纯手工写脚本高很多。9.3 从日志中识别攻击痕迹作为防御方,识别日志中的Cookie注入攻击特征同样很重要。一般的攻击请求会在Cookie值中出现单引号、布尔表达式、UNION关键字、注释符等异常数据。如果你维护的应用有访问日志系统,可以定期针对Cookie字段做规则告警。常见的检测规则包括:Cookie值中包含SQL关键字:union、select、and、orCookie值中包含注释符:--、#、/*Cookie值中包含编码后的特殊符号:%27、%3B、%23Cookie值中包含数据库函数调用:sleep、version、benchmark把上述规则做成简单的正则或告警策略,通常能发现大半的试探性攻击。这里我举一个常见的日志样例,假设某次访问日志中的Cookie字段出现了userType=1 AND SLEEP(5),那基本可以判定为有人在探测时间盲注。10. 个人实操过程中的一点体会聊完了整套Cookie注入的原理、测试方法和防御方案,最后说点实在的。我在实际做安全评估时,Cookie注入点出现的位置常常让人意想不到。有一个印象很深的例子:某系统的语言切换功能把语言标识存在Cookie里,开发者用这个值去动态拼接一个SQL语句的表名(因为系统做了多语言分表),结果语言标识成了注入点。这个案例让我明白一个道理——只要你在SQL语句中拼接任何来自客户端的数据,不管这数据是URL参数、表单字段,还是Cookie,注入风险就客观存在。Cookie只是载体之一,真正的洞在“拼SQL”这件事本身。所以我常跟团队里的新人讲一句话:“参数化查询不是一种高级技巧,而是一条必须刻进肌肉记忆的底线。”你只要看到代码里有字符串拼接SQL,第一反应就应当警惕;看到从$_COOKIE或request.getCookies()里取值去拼SQL,直接判死刑,无需争论。安全建设本质上是消灭侥幸心理的过程。对于做安全测试的朋友,我的建议是不要只依赖自动化工具。扫描器能覆盖的注入点是有限的,Cookie注入这种需要人工构造请求的场景,恰恰是体现测试人员价值的地方。花时间把一个手工注入的完整流程练熟,远比漫无目的地跑几百个自动化扫描任务有意义。
1. 前言:为什么Cookie会成为攻击者的突破口在做Web安全测试时,很多人习惯性地只关注URL参数和表单提交的数据,却忽略了一个非常关键的输入点——Cookie。Cookie注入攻击的原理其实并不复杂,但它的隐蔽性极强,很多开发者在做安全防护时,会把SQL注入的过滤逻辑写得很完善,却唯独漏掉了Cookie这个入口。结果就是应用的前端功能稳如泰山,攻击者却在Cookie上轻松撕开了一道口子。这篇文章是《Web安全原理剖析》系列的第九篇,重点讨论Cookie注入攻击的完整链路。我会从Cookie的本质上手,拆解它为什么会成为注入的高危入口,然后结合真实的测试场景,展示一个典型的Cookie注入攻击是如何从识别、利用到提取数据的全过程,最后给出对应的防御方案。无论你是做安全测试、渗透测试,还是负责应用开发和运维,弄清楚这条攻击路径都非常有必要——因为你大概率正在维护的某个系统,Cookie头可能就是一个不设防的后门。2. 深入理解Cookie注入的本质在讲具体的攻击手法之前,必须先理清一个问题:Cookie和SQL之间到底存在什么关系,为什么一个看似无害的小文本文件能够成为攻击者注入SQL语句的载体?2.1 无状态协议与Cookie的诞生HTTP协议设计之初就是无状态的,这意味着服务器无法区分两次独立的请求是否来自同一个用户。打个比方,你去一家自习室学习,管理员第一次见到你,登记了你的名字,但第二天他完全不记得你是谁了——你需要重新登记一遍。这显然很低效。于是Cookie被设计出来,用来让服务器“记住”用户。当用户第一次访问站点时,服务器会在响应头里写入Set-Cookie,浏览器将其保存在本地。后续的每一次请求,浏览器都会自动携带这个Cookie,服务器拿到Cookie后就可以判断当前请求的身份状态。这种机制本身没有问题,但问题在于:服务器接收到的Cookie数据,本质上和你在地址栏输入的参数没有任何区别——它同样是一条由客户端控制的数据流。2.2 SQL注入对输入来源的宽容性我们熟悉的URL参数注入、POST表单注入,本质上都是因为开发者把用户的输入未经严格过滤就拼接进了SQL语句。比如一个登录功能:$sql = "SELECT * FROM users WHERE username = '" . $_POST['username'] . "'";这里存在典型的SQL注入。但如果开发者把同样的逻辑用在Cookie数据上:$sql = "SELECT * FROM users WHERE user_id = '" . $_COOKIE['user_id'] . "'";问题就会完全一样地出现在Cookie上。Cookie注入的本质就是:开发者默认Cookie数据是“安全”的,认为只有服务器自己写入的内容才存在Cookie里,客户端无法篡改。这是一个致命误解——Cookie完全由客户端掌控,任何攻击者都可以手工构造一个包含恶意SQL语句的Cookie值,然后在请求中发送给服务器。2.3 从攻击面视角看Cookie注入的价值从攻击者的视角看,Cookie注入有几个非常诱人的特点:隐蔽性高:很多WAF(Web应用防火墙)会重点检查GET和POST参数,对Cookie的检查相对宽松,甚至完全忽略。命中率高:很多开发者对GET参数做了过滤,但使用Cookie读取的参数往往直接在SQL语句中拼接。自动化绕过:传统的SQL注入扫描器很多不会主动发送恶意Cookie,导致自动化工具扫不出漏洞,但人工测试可以精确命中。可绕过登录限制:很多应用的Cookie中存储了用户身份标识(如ID),如果注入点在身份标识上,攻击者甚至可以越权提取其他用户的数据。这些特点决定了Cookie注入在真实攻击中的利用率极高。我做过不少授权范围的安全评估项目,Cookie注入出现的频率远超大多数人的想象,而且因为隐蔽性强,往往会存在很久才被发现。3. Cookie注入的分类与核心场景Cookie注入的SQL语句形态和普通SQL注入是一样的,只是入口不同。常见的Cookie注入主要有以下几种典型场景。3.1 整数型Cookie注入这是最简单、也最容易出漏洞的形式。典型代码如下:$userId = $_COOKIE['user_id']; $sql = "SELECT * FROM users WHERE user_id = " . $userId;如果用户在Cookie中设置user_id = 1 OR 1=1 --,那拼接出来的SQL就变成了:SELECT * FROM users WHERE user_id = 1 OR 1=1 --这里--是SQL注释符,后面的内容全部被注释掉。这个语句会把users表的所有记录全部查询出来。如果应用在登录状态判断时使用了这个查询结果,就可能直接绕过验证。3.2 字符型Cookie注入另一种常见的形式是在字符串拼接场景下。典型代码如下:$username = $_COOKIE['username']; $sql = "SELECT * FROM users WHERE username = '" . $username . "'";攻击者在Cookie中填入admin' --,拼出来的SQL就变成:SELECT * FROM users WHERE username = 'admin' -- '这个攻击方式是直接以管理员的身份登录。我见过不少后台管理系统用Cookie存储用户名,而开发者在拼接用户名时没有做任何转义,导致一个简单的请求头就把整个后台大门敞开。3.3 搜索、筛选与排序场景还有一种容易被忽略的场景是数据列表页。很多列表页会把搜索关键字、排序字段、分页编号保存在Cookie中,方便用户下次访问时记住上次的状态。比如某个后台订单列表,它会读取Cookie中的orderBy字段来做排序:$orderField = $_COOKIE['order_field']; $sql = "SELECT * FROM orders ORDER BY " . $orderField;这种情况下,攻击者可以把order_field设置为1; DROP TABLE orders --,虽然这种堆叠注入在部分数据库方言下无法执行,但每一句恶意排序字段都可能造成信息泄露或数据损坏。4. 手工检测Cookie注入的完整思路在实际测试中,要判断Cookie是否存在注入,需要遵循一套严谨的流程。按下述步骤操作,绝大多数隐蔽的Cookie注入点都会现出原形。4.1 环境准备与授权确认如果你是安全测试人员或学习者,请务必在自己的测试环境,或者获得授权的测试环境中进行。我一般会搭建一个本地靶场环境,或者使用现成的漏洞靶场来练习。这一步的目的是确保攻击行为完全合法合规。工具方面,你只需要一个浏览器和一个能够自由修改请求的工具。浏览器自带的开发者工具能看请求响应,而修改Cookie则需要借助一些浏览器扩展,或者使用功能更完整的HTTP请求工具(如Burp Suite)。4.2 收集站点Cookie结构第一步是正常访问应用,打开开发者工具,观察请求头中的Cookie字段。注意每个Cookie的名称、数量和取值规律。重点关注的Cookie类型包括:user_id、uid、userId这类身份标识型Cookieusername、uname这类用户名型Cookietoken、session、auth这类认证信息型Cookielang、theme、page、order这类功能和偏好型Cookie对于上面每一种Cookie,初步判断它是否会被用作SQL语句的拼接数据。Cookie名称本身就是一个提示信号,比如cat_id这种,几乎可以肯定它会关联数据库查询。4.3 单引号探测法对每一个可疑Cookie,用单引号替换原有值,观察应用响应是否有异常。以cat_id=2为例:原始请求: Cookie: cat_id=2 探测请求: Cookie: cat_id=2'如果cat_id直接拼接进SQL,数据库会报语法错误,页面可能出现数据库错误信息,也可能出现500状态,或者服务器直接返回一个空白页。无论哪种异常,都值得进一步验证。我见过一种特殊情况:页面不报错,但返回了正常的空内容。这种情况往往是因为SQL语法错误被应用层吞掉了,数据库查询失败,代码没有处理就继续执行。这种“平静的异常”同样需要高度重视,要结合时间延迟等方式来进一步判断。4.4 逻辑判断探测法单引号报错之后,接着用逻辑表达式来验证。对于一个整数型注入点,把Cookie值改为2 AND 1=1,页面应正常;再改为2 AND 1=2,页面应异常。两次响应有差异,就能确定这里的Cookie值被拼进了SQL。对于字符型注入点,先构造2' AND '1'='1和2' AND '1'='2来做对比测试。注意字符型注入在拼接时需要处理好引号的配平关系。4.5 时间盲注探测法如果页面不显示任何错误信息,也没有逻辑差异,可以试试时间盲注。把Cookie值改成:2 AND SLEEP(5) --(MySQL写法)2 AND pg_sleep(5) --(PostgreSQL写法)如果页面响应时间明显延迟了大约5秒,说明语句被成功执行,这里可以确定存在注入点,而且使用的是时间盲注方式。时间盲注在实际测试中非常有用,但要注意控制单个探测点的延时时长,避免产生大量请求导致日志异常或服务压力。一般我会用3秒或者5秒,既能明显感知,又不至于拖垮目标。5. 从识别到利用的完整实例拆解判断出注入点之后,接下来就是利用的过程。这里我用一个典型场景来走一遍完整流程,让你直观地看到Cookie注入能造成多大的危害。5.1 场景设定与初步判定假设某个内容管理系统(CMS),用户登录后,系统在Cookie中写入了一个userType字段。普通用户的默认值是1,管理员的值是0。系统后台代码中有如下逻辑:$userType = $_COOKIE['userType']; $sql = "SELECT * FROM content WHERE user_type = " . $userType . " AND status = 1";这个SQL在应用中的作用是:根据用户的类型,加载其有权限查看的内容。我们先用单引号探测:Cookie: userType=1'页面返回数据库异常。再用逻辑判断:Cookie: userType=1 AND 1=1 页面正常,内容正常加载。 Cookie: userType=1 AND 1=2 页面空内容,没有显示任何数据。这就能确认:Cookie中的userType直接拼入了SQL语句,且是整数型注入。5.2 判断字段数量并完成联合查询要用联合查询提取数据,需要先知道查询字段的数量。用普通的ORDER BY探测法就能搞定:Cookie: userType=1 ORDER BY 1 -- 正常 Cookie: userType=1 ORDER BY 5 -- 正常 Cookie: userType=1 ORDER BY 10 -- 报错通过逐级递增测试,确定查询共有9个字段。然后构造联合查询:Cookie: userType=1 UNION SELECT 1,2,3,4,5,6,7,8,9 --页面显示了内容位置2、4、7上的数字,说明这3个字段会被输出到页面上。于是把对应位置替换成我们需要的数据:Cookie: userType=1 UNION SELECT 1,version(),3,database(),5,6,user(),8,9 --实际执行后,页面回显了数据库版本、当前数据库名、数据库用户等信息。到这里,一个基本的Cookie注入点已经可以被完整利用了。5.3 提取核心敏感数据因为注入点在CMS的内容查询上,我们可以进一步读取其中的管理员表。先猜测表名为admin_users,字段名为username和password:Cookie: userType=1 UNION SELECT 1,username,3,password,5,6,group_concat(username,0x3a,password),8,9 FROM admin_users --这里的0x3a是冒号:的十六进制,用于在输出时把用户名和密码拼接成可读的一行。如果管理员密码是明文存储,这里会直接暴露明文。如果密码是哈希,攻击者后续可以离线破解。这一步做完,你对Cookie注入的威胁程度就有了清晰的认知:一个排除了所有前端过滤的Cookie值,直接把后端数据库的表结构和核心数据全部暴露给攻击者。6. 不同数据库方言下的Cookie注入差异Cookie注入利用手法的实现,高度依赖于后端使用的数据库类型。不同数据库的语法方言、注释方式和字符串拼接规则都有所不同,实际测试时需要区别对待。6.1 MySQL / MariaDB这是国内Web应用使用最广泛的数据库组合。常用特性:注释符支持#、--(注意--后面必须跟一个空格)和/*...*/信息获取函数:version()、database()、user()、@@version_compile_os字符串拼接函数:CONCAT()、CONCAT_WS()延时函数:SLEEP(n)、BENCHMARK(n, expr)MySQL还有一个非常有用的能力——在UNION SELECT中直接使用GROUP_CONCAT()把多行数据合并成一行,方便读取。我在上面的实例中就用了这个方法。6.2 Oracle数据库Oracle在大型企业系统中仍然占有相当大的份额。Oracle和MySQL的SQL方言差异很大:Oracle没有LIMIT,使用的是ROWNUM,且ROWNUM的取值逻辑和LIMIT完全不同。注释符只支持--,不支持#。表名、字段名查询需要从系统视图ALL_TABLES、ALL_TAB_COLUMNS中读取。字符串拼接用的是||操作符,比如SELECT 'a' || 'b' FROM dual。延时函数可以使用DBMS_LOCK.SLEEP(n),但这个函数需要合适的权限才能执行。在Oracle场景下,Cookie注入的利用流程会更复杂一些,尤其是数据提取需要依赖系统视图,SQL语句会变得很长,手工测试的效率会明显降低。6.3 SQL ServerSQL Server主要出现在Windows系统架构中。它有几个特殊的地方:注释符支持--和/*...*/。系统函数DB_NAME()返回当前数据库名,@@VERSION返回版本信息。分号堆叠执行的限制相对较少,在特定条件下可以执行多条语句。SQL Server的字符串拼接用+操作符。延时参考使用WAITFOR DELAY '0:0:5'。SQL Server场景下有一种经典的利用方式:如果后端是ASP或.NET应用,且数据库连接权限较高,攻击者甚至可以通过注入开启xp_cmdshell来执行系统命令。虽然大多数现代配置会禁用该组件,但如果运维人员图省事保留了默认权限,这种攻击会导致严重的系统失陷。这里我仅仅是提示这种可能性,读者务必清楚这是一条高危链路,只在授权环境下测试。6.4 SQLiteSQLite常见于小型应用、嵌入式场景和部分桌面级Web应用。它的SQL语法接近MySQL,但有一些限制:没有version()函数,但可以通过sqlite_version()获取版本。不支持MySQL风格的UNION SELECT字符串拼接函数(没有CONCAT()),但可以用||来实现拼接。系统表是sqlite_master,用来存储表结构信息。注释符支持--和/*...*/。SQLite注入的利用会受限于它的数据类型,因为它的列类型是弱约束的,有时候联合查询的数据类型匹配更加宽松,反而更容易构造Payload。7. 绕过过滤与WAF的进阶手法如果目标系统对Cookie输入做了些过滤,或者部署了WAF,基础注入Payload会被拦截。这时候就需要对Payload进行变形,以提高绕过概率。7.1 大小写、注释与编码变形当过滤规则是关键字匹配时,大小写混写经常能直接绕过:union select → UnIoN SeLeCt如果大小写也被过滤,可以用内联注释分割关键字:un/**/ion sel/**/ectMySQL支持在关键字中间插入注释,注释部分会被忽略,但拼接后的SQL语句不受影响。URL编码是另一个方向。有些WAF对GET参数的解码很积极,对Cookie参数的解码则不够全面。可以把Payload中的部分字符做URL编码,看后端能否正确解码。我遇到过一些用ASP.NET搭建的系统,Cookie中传入URL编码后的单引号%27,后端框架会自动解码,但WAF层却绕过去了。7.2 等价替换函数很多过滤规则是针对特定函数名的,比如封禁sleep、version、database。这时可以用功能等价的替代方案:用SUBSTR()替代MID(),或者反过来。用BENCHMARK()替代sleep()做延时。用@@datadir、@@hostname这类系统变量替代函数获取信息。用十六进制表示字符串,绕过针对引号的过滤,比如0x75736572表示user字符串。需要说明的是,绕过WAF是一个动态博弈过程,没有包打天下的Payload。关键在于理解WAF的解析逻辑和后端数据库的解析逻辑之间的差异,利用两者的不对称性来寻找绕过点。7.3 利用注释符截断后续语句字符型注入时,Payload结尾经常要用注释符把后面的引号和SQL语句截断掉。不同数据库的注释符不兼容,这也是一个容易被忽略的细节。比如在MySQL下用#,在Oracle下就必须换成--,少一个空格都可能导致语法错误。很多新手在做跨数据库测试时,经常因为注释符问题白白浪费时间,排查半天才发现是注释符不匹配。8. 防御方案设计:从源头封堵Cookie注入防守方的思路其实不复杂:把攻击的所有入口都堵上,重点把握好以下三个环节。8.1 参数化查询是根治手段防御SQL注入的根本手段是使用参数化查询(Prepared Statement),让SQL语句的结构和参数数据完全分离。以下以PHP的PDO为例:$stmt = $pdo->prepare("SELECT * FROM content WHERE user_type = ? AND status = 1"); $stmt->execute([$_COOKIE['userType']]);参数化查询会确保传入的值只作为数据处理,永远不会被当作SQL代码解析。无论攻击者在Cookie中放什么内容,都会被当作普通字符串传给数据库。对于Java开发者,可以使用PreparedStatement;Python开发者可以使用数据库驱动自带的参数绑定;.Net开发者则使用SqlCommand的参数集合。所有主流语言和框架都提供了参数化查询的支持,关键是要养成习惯,凡是输入数据一律走参数化。8.2 输入校验与白名单机制有些场景不宜直接套用参数化(比如动态排序字段),这时候必须用白名单校验逻辑。比如排序字段,不可能靠参数化解决问题,因为字段名不应当是用户自由输入的内容,而应当交给白名单:$allowedOrderFields = ['create_time', 'order_amount', 'order_status']; if (in_array($_COOKIE['order_field'], $allowedOrderFields, true)) { $orderField = $_COOKIE['order_field']; } else { $orderField = 'create_time'; }这里使用严格白名单,从源头上消除了恶意字段名的可能性。对于数字型数据,可以用is_numeric()或者强制类型转换。8.3 安全配置与纵深防御除了代码层面的修复,运行环境的安全配置同样重要:数据库账户权限最小化:连接数据库的应用账户,只授予其业务需要的SELECT、INSERT、UPDATE、DELETE权限,绝不授予DDL权限(如DROP、ALTER),更不授予FILE权限。这样即使注入成功,攻击者能造成的破坏也极其有限。关闭数据库错误回显:生产环境不要向用户展示SQL错误详情。WAF规则优化:确保Cookie字段也在检测范围内,且要对URL编码、混合编码做解码分析。定期安全扫描:不要只扫描URL参数,还要主动构造异常的Cookie请求做模糊测试。很多时候我发现系统出现SQL注入,不是由某个“高级漏洞”导致的,而是多个基础安全配置疏忽叠加的结果。如果把上面的每一项都做到位,这个站点的整体安全水位会立刻提升一个档次。9. 常见问题与排查技巧实录做Cookie注入测试和修复的这些年里,有一些高频问题反复出现。这里总结成一个速查对照表,方便你平时排查。9.1 常见问题对照表问题现象可能原因排查方向页面返回500,但是日志无SQL错误应用层主动吞掉了数据库异常开启数据库错误日志,或使用时间盲注验证单引号测试无反应输入被框架转义或过滤尝试二次编码或等价替换函数绕过联合查询字段数对,但页面无回显回显位不在当前页面模板中换一种输出方式,或改用布尔盲注延时注入无延迟Cookie值未到达SQL拼接点检查Cookie值是否被服务端修改或丢弃始终返回相同页面WAF对Cookie做静态检测尝试编码、注释、大小写混写绕过手工测试正常,扫描器测不出扫描器默认不测试Cookie人工测试始终是最可靠的手段9.2 容易被忽略的测试陷阱Cookie在传输过程中可能被多个中间环节修改,这会直接影响注入测试的稳定性和准确率。比如有些负载均衡器或反向代理会在请求转发时自动清理部分Cookie;有些框架会对Cookie做签名校验,Cookie值被修改后直接被丢弃,导致检测结果失真。还有一种情况值得注意:Cookie中可能保存了多个属性,比如Path、Domain、HttpOnly、Secure等。这些属性只影响浏览器行为,不会影响服务器端对Cookie值的处理。但测试时如果你用脚本直接修改Cookie值,一定要确保修改后的请求仍然背着完整的Cookie字段,不要因为格式问题导致请求被服务器拒收。另外,修改Cookie时要注意处理器的细节。以Burp Suite为例,它在History面板中可以直接右键修改请求并重放。修改后的Cookie会完整保存下来,方便快速迭代测试。这个工具链是我每次做Cookie注入测试时的标配,效率比纯手工写脚本高很多。9.3 从日志中识别攻击痕迹作为防御方,识别日志中的Cookie注入攻击特征同样很重要。一般的攻击请求会在Cookie值中出现单引号、布尔表达式、UNION关键字、注释符等异常数据。如果你维护的应用有访问日志系统,可以定期针对Cookie字段做规则告警。常见的检测规则包括:Cookie值中包含SQL关键字:union、select、and、orCookie值中包含注释符:--、#、/*Cookie值中包含编码后的特殊符号:%27、%3B、%23Cookie值中包含数据库函数调用:sleep、version、benchmark把上述规则做成简单的正则或告警策略,通常能发现大半的试探性攻击。这里我举一个常见的日志样例,假设某次访问日志中的Cookie字段出现了userType=1 AND SLEEP(5),那基本可以判定为有人在探测时间盲注。10. 个人实操过程中的一点体会聊完了整套Cookie注入的原理、测试方法和防御方案,最后说点实在的。我在实际做安全评估时,Cookie注入点出现的位置常常让人意想不到。有一个印象很深的例子:某系统的语言切换功能把语言标识存在Cookie里,开发者用这个值去动态拼接一个SQL语句的表名(因为系统做了多语言分表),结果语言标识成了注入点。这个案例让我明白一个道理——只要你在SQL语句中拼接任何来自客户端的数据,不管这数据是URL参数、表单字段,还是Cookie,注入风险就客观存在。Cookie只是载体之一,真正的洞在“拼SQL”这件事本身。所以我常跟团队里的新人讲一句话:“参数化查询不是一种高级技巧,而是一条必须刻进肌肉记忆的底线。”你只要看到代码里有字符串拼接SQL,第一反应就应当警惕;看到从$_COOKIE或request.getCookies()里取值去拼SQL,直接判死刑,无需争论。安全建设本质上是消灭侥幸心理的过程。对于做安全测试的朋友,我的建议是不要只依赖自动化工具。扫描器能覆盖的注入点是有限的,Cookie注入这种需要人工构造请求的场景,恰恰是体现测试人员价值的地方。花时间把一个手工注入的完整流程练熟,远比漫无目的地跑几百个自动化扫描任务有意义。