引言
想象一下,你无需密码就能登录他人的社交账号、窃取他们的购物车,甚至进行支付操作。这听起来像好莱坞电影,但实际上,通过一种叫做 Cookie注入 的攻击,这完全可以实现
是网站用来识别用户“身份”的凭证,是维持会话(Session)的核心
本文将带你深入了解Cookie注入是什么、它是如何发生的、黑客如何利用它,以及最重要的——我们如何防御它
什么是HTTP Cookie
用一个比喻带你深入了解一下
想象一下你去一家奶茶店:
- 第一次光临:你点了一杯奶茶。店员发现你没有会员卡,于是给你办了一张新卡,并在卡上写下一串唯一的号码(例如 #114514)。同时,他们在店里的账本上记下:“卡号 #114514 的顾客喜欢芝士葡萄,积分 10”。然后他们把这张卡给你
- 保存卡片:你离开时,把这张会员卡(Cookie)放进了自己的钱包(浏览器)里
- 再次光临:一周后,你再次来到这家店。一进门,你就出示了这张会员卡(浏览器在请求中自动发送 Cookie)。店员看到卡号 #114514,立刻去查账本(服务器上的数据库)。然后马上说:“王先生您好!还是老规矩,一杯芝士葡萄吗?您目前有 10 积分。”
- 无卡光临:如果你下次没带这张卡,店员就会把你当作新顾客,流程又重新开始
技术定义
HTTP Cookie(通常直接称为 Cookie)是一小段由服务器发送到用户浏览器并保存在本地的数据
- 服务器生成:当你访问一个网站时,网站的服务器可以决定生成一个或多个 Cookie
- 浏览器存储:你的浏览器会接收这些 Cookie 并将它们存储在你的电脑上
- 自动发送:此后,每当浏览器再向同一服务器发起请求时,它会自动地把这些 Cookie 附加在 HTTP 请求头中一起发送回去
这样,服务器就能“记住”之前在你浏览器上发生过的信息(如登录状态、偏好设置等),从而实现有状态的会话,而 HTTP 协议本身是无状态的(服务器默认不记得之前的请求)
Cookie 的用途
Cookie 最初是为了解决“记住用户状态”的问题而发明的,现在主要有以下用途:
会话管理(最核心的用途)
- 用户登录状态:保持用户的登录信息。服务器在你登录后发送一个包含“会话ID”的 Cookie,浏览器下次带着这个 ID 来,服务器就知道你是谁了,例如:每次进入CSDN就会有自己账号的信息
- 购物车内容:在电商网站,即使你跳转到其他页面或刷新,购物车里的商品也不会消失,就是因为商品信息被存在 Cookie 里
个性化
- 用户偏好:记住你的语言设置、主题(深色/浅色模式)、字体大小等
- 内容推荐:根据你过去的浏览习惯,展示你可能感兴趣的内容
跟踪与分析
- 广告追踪:广告商利用第三方 Cookie 来追踪你在不同网站上的行为,从而向你投放更相关(或者说更精准)的广告。这也是 Cookie 常常与隐私问题关联的原因
- 网站分析:帮助网站所有者分析用户行为,了解流量来源、用户停留时间等,例如 Google Analytics 就使用 Cookie
Cookie 的关键属性
一个 Cookie 不只是一段数据,它还包含一些控制其行为的属性,由服务器在设置时指定:
名称(Name)和值(Value):实际的数据内容,都是字符串格式(例如 username=john_doe)
过期时间(Expires/Max-Age):
- 会话期 Cookie (Session Cookie):不设置 Expires 或 Max-Age。这种 Cookie 在浏览器关闭后就会被删除。就像咖啡店的会员卡只在你本次消费期间有效
- 持久性 Cookie (Persistent Cookie):设置了具体的过期时间。即使关闭浏览器,它也会保存在电脑上,直到过期。就像一张一年内有效的实体会员卡
域(Domain)和路径(Path):
- 定义了 Cookie 应该被发送到哪个域名和路径下。例如,设置为 .hsqg.com 的 Cookie 会被发送给 www.hsqg.com、api.hsqg.com 等所有子域名
安全标志(Secure):
- 设置了 Secure 的 Cookie只能通过 HTTPS 加密连接传输,防止在传输过程中被窃听
HttpOnly 标志:
- 设置了 HttpOnly 的 Cookie 无法 通过 JavaScript 的 Document.cookie API 访问。这是重要的安全措施,可以有效防止跨站脚本(XSS)攻击窃取 Cookie 信息
SameSite 标志(现代浏览器重要安全属性):
- Strict:Cookie 仅在与当前页面域名相同的第一方请求中发送,完全禁止第三方上下文发送
- Lax:(默认值)在跨站请求中,只允许在安全且“顶级导航”(如点击链接)的情况下发送 Cookie。这防止了 CSRF 攻击的大部分情况,同时不影响用户体验
- None:Cookie 在所有上下文中发送,包括跨站请求(如嵌入的 iframe、图片),必须与 Secure 属性一同设置(即必须使用 HTTPS)
隐私与安全考量
- 隐私问题:主要是第三方 Cookie 引起的。第三方服务器可以设置和读取它们自己的 Cookie,从而构建你的跨站浏览画像。现代浏览器正逐步淘汰第三方 Cookie
- 安全风险:
- XSS(跨站脚本):窃取未设置 HttpOnly 的 Cookie
- CSRF(跨站请求伪造):利用用户已存在的登录 Cookie 发起非预期请求,SameSite 属性是解决此问题的主要手段
小结
HTTP Cookie 是一个由服务器创建、由浏览器存储、并在后续请求中自动发回给服务器的数据片段。它的核心作用是让无状态的 HTTP 协议能够“记住”信息,从而实现登录状态、偏好设置等关键功能,但同时它也带来了隐私追踪和安全方面的挑战
知识拓展
会话(Session)与Cookie的关系
会话(Session)和 Cookie 的关系是协同工作、相辅相成的
咱来用一个非常经典的比喻来理解:
Session 是商场(服务器)里的「储物柜」
- 这个储物柜有一个唯一的编号(例如 #1314)
- 柜子里可以存放你的各种物品(用户数据,如用户ID、用户名、购物车信息、登录状态等)
- 所有这些储物柜都放在商场后场的储物区(服务器的内存、数据库或缓存中)
Cookie 是商场前台发给你的「钥匙卡」
- 当你第一次来到商场(访问网站),商场前台(服务器)看你没有钥匙卡,就为你开了一个新的储物柜 #1314,并把里面暂时放上你的信息
- 然后,前台给你一张只写了储物柜编号 #1314 的钥匙卡。这张卡本身不存放你的任何物品,只告诉商场你的柜子是哪一个
- 你离开时,把这张钥匙卡(Cookie)放进口袋(浏览器)
你再次光临商场:
- 你一进门(发起新的请求),自动出示了你的钥匙卡(浏览器自动在请求头中携带Cookie)
- 商场前台(服务器)看到钥匙卡上的编号 #1314,就去后场的储物区找到对应的 #1314 储物柜,拿出里面的物品,就知道你是谁、你的喜好和之前的行为了
技术性理解
特性 | Cookie | Session |
---|---|---|
存储位置 | 客户端(用户的浏览器中) | 服务端(服务器的内存、数据库或专用缓存如Redis中) |
存储内容 | 一个唯一的标识符,即 Session ID | 所有的用户数据(如 {user_id: 123, username: ‘alice’, is_logged_in: true}) |
安全性 | 相对较低,数据存储在客户端,可能被查看或篡改,不应存储敏感信息。 | 相对较高,关键数据存储在服务端,客户端无法直接访问或修改 |
生命周期 | 可以设置很长的过期时间(如“记住我”功能),即使浏览器关闭也可能存在 | 通常有失效时间(如用户 inactivity 30分钟后,Session自动过期)服务器可以主动销毁Session |
性能影响 | 不占用服务器资源。但每次HTTP请求都会携带,增加带宽开销 | 占用服务器资源,用户量巨大时,需要精心管理存储方案(如使用Redis)以避免内存耗尽 |
它们如何协同工作
一句话概括:
Session 是一种在服务端存储用户状态的机制,而 Cookie 是在客户端用于维持这种状态(通过传递Session ID)的主流实现方式
Cookie注入的前置条件
- 应用程序存在漏洞:信任并使用了Cookie中的数据
这是最核心的条件。应用程序的业务逻辑必须直接将Cookie中的某个或多个参数的值,未经充分安全处理地拼接到SQL查询语句中
- 常见场景:
- 用于身份认证的 user_id、username
- 用于个性化内容的 preferences、theme
- 用于跟踪的 session_id
- 输入处理缺失:未对Cookie数据进行过滤或转义
应用程序必须缺乏有效的输入验证、过滤和转义机制
缺失的防护:
- 未使用参数化查询(Prepared Statements):这是最根本的防御措施。如果用了参数化查询,即使Cookie被修改,其内容也只会被当作数据而非代码执行
- 未进行类型转换:例如,如果 user_id 本应是整数,程序却没有将其强制转换为整型 (int)$_COOKIE[‘user_id’]
- 未转义特殊字符:没有对单引号 '、注释符 –、/**/ 等SQL元字符进行转义或过滤
- 攻击面可用:Cookie参数可被预测和操控
攻击者必须能够修改目标Cookie的值,并将其发送到存在漏洞的服务器。
修改方式:
- 浏览器开发者工具:现代浏览器都提供可随时修改Cookie的功能
- 浏览器插件/扩展:可以更方便地管理和编辑Cookie
- 拦截代理工具:如Burp Suite 在HTTP请求发送到服务器前拦截并修改其中的Cookie值,这是专业渗透测试最常用的方法
- 利用XSS漏洞:如果网站同时存在XSS(跨站脚本)漏洞,攻击者可以编写恶意脚本,悄悄地修改用户的Cookie,从而发起更隐蔽的攻击
- 信息反馈:有可观测的输出或错误信息
攻击通常是一个“试错”过程,攻击者需要根据服务器的响应来调整攻击载荷(Payload)。反馈方式有两种:
- 显式反馈(经典SQL注入):服务器将SQL查询的错误信息直接返回给页面。这使攻击者能快速了解SQL语句的结构,极大地便利了攻击
- 隐式反馈(盲注 - Blind SQL Injection):页面不会返回数据库错误信息,但会根据注入的SQL语句的逻辑真假表现出不同的行为(如返回正常页面、404错误、不同的响应时间等)。攻击者通过观察这些细微差异来推断注入是否成功。虽然难度更大、更耗时,但仍然是可行的
核心思想:只要破坏了上述条件中的任意一个,尤其是条件1和2,Cookie注入攻击就无法成功。而使用参数化查询是同时破坏条件1和2的最有效、最根本的解决方案(防御亦是如此)
Cookie注入详解
核心概念
Cookie注入是SQL注入攻击的一种特殊形式,它与传统SQL注入的核心原理完全相同——都是利用应用程序未对用户输入进行充分过滤和转义的漏洞,将恶意构造的SQL代码注入到后台数据库中执行
它们唯一的区别在于 注入点(攻击入口) 不同:
传统SQL注入:恶意代码通过用户可见的输入点传入,如:
- GET 请求的URL参数(example.com?id=1)
- POST 请求的表单字段(如登录框、搜索框)
Cookie注入:恶意代码通过HTTP请求中的Cookie字段传入
- 不过跟User-Agent注入,Referer注入一样
Cookie注入攻击的原理
Cookie注入的本质是:攻击者通过篡改浏览器中的Cookie数据,将恶意SQL代码传递到服务器。由于服务器应用程序盲目信任Cookie中的数据,并未经任何过滤就将其拼接到SQL语句中执行,从而导致数据库被恶意查询,还是跟其他注入原理一样
它是一种SQL注入攻击,只是攻击的输入向量(Input Vector) 从常见的表单(Form)或URL参数(GET)变成了HTTP请求头中的 Cookie
就以本次靶场less-20的源代码举例,来看看到底为什么存在注入
if(isset($_POST['uname']) &&
isset($_POST['passwd']))
{
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
$result1 = mysql_query($sql);
$row1 = mysql_fetch_array($result1);
$cookee = $row1['username'];
if($row1)
{
echo '<font color= "#FFFF00" font size = 3 >';
setcookie('uname', $cookee, time()+3600);
header ('Location: index.php');
echo "I LOVE YOU COOKIES";
echo "</font>";
echo '<font color= "#0000ff" font size = 3 >';
//echo 'Your Cookie is: ' .$cookee;
echo "</font>";
echo "<br>";
print_r(mysql_error());
echo "<br><br>";
echo '<img src="../images/flag.jpg" />';
echo "<br>";
}
else
{
echo '<font color= "#0000ff" font size="3">';
//echo "Try again looser";
print_r(mysql_error());
echo "</br>";
echo "</br>";
echo '<img src="../images/slap.jpg" />';
echo "</font>";
}
}
echo "</font>";
echo '</font>';
echo '</div>';
}
else
{
if(!isset($_POST['submit']))
{
$cookee = $_COOKIE['uname'];
$format = 'D d M Y - H:i:s';
$timestamp = time() + 3600;
echo "<center>";
echo '<br><br><br>';
echo '<img src="../images/Less-20.jpg" />';
echo "<br><br><b>";
echo '<br><font color= "red" font size="4">';
echo "YOUR USER AGENT IS : ".$_SERVER['HTTP_USER_AGENT'];
echo "</font><br>";
echo '<font color= "cyan" font size="4">';
echo "YOUR IP ADDRESS IS : ".$_SERVER['REMOTE_ADDR'];
echo "</font><br>";
echo '<font color= "#FFFF00" font size = 4 >';
echo "DELETE YOUR COOKIE OR WAIT FOR IT TO EXPIRE <br>";
echo '<font color= "orange" font size = 5 >';
echo "YOUR COOKIE : uname = $cookee and expires: " . date($format, $timestamp);
echo "<br></font>";
$sql="SELECT * FROM users WHERE username='$cookee' LIMIT 0,1";
$result=mysql_query($sql);
if (!$result)
{
die('Issue with your mysql: ' . mysql_error());
}
$row = mysql_fetch_array($result);
if($row)
{
echo '<font color= "pink" font size="5">';
echo 'Your Login name:'. $row['username'];
echo "<br>";
echo '<font color= "grey" font size="5">';
echo 'Your Password:' .$row['password'];
echo "</font></b>";
echo "<br>";
echo 'Your ID:' .$row['id'];
}
else
{
echo "<center>";
echo '<br><br><br>';
echo '<img src="../images/slap1.jpg" />';
echo "<br><br><b>";
//echo '<img src="../images/Less-20.jpg" />';
}
echo '<center>';
echo '<form action="" method="post">';
echo '<input type="submit" name="submit" value="Delete Your Cookie!" />';
echo '</form>';
echo '</center>';
}
else
{
echo '<center>';
echo "<br>";
echo "<br>";
echo "<br>";
echo "<br>";
echo "<br>";
echo "<br>";
echo '<font color= "#FFFF00" font size = 6 >';
echo " Your Cookie is deleted";
setcookie('uname', $row1['username'], time()-3600);
header ('Location: index.php');
echo '</font></center></br>';
}
echo "<br>";
echo "<br>";
//header ('Location: main.php');
echo "<br>";
echo "<br>";
//echo '<img src="../images/slap.jpg" /></center>';
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'Cookie:'.$cookee."\n");
fclose($fp);
}
最重要的也就这几行代码:
将用户输入的用户名和密码与数据库进行对比查询,查看是否一致
$sql="SELECT users.username, users.password FROM users WHERE users.username=$uname and users.password=$passwd ORDER BY users.id DESC LIMIT 0,1";
将拼接好的SQL语句发送到MySQL数据库执行,并从查询结果中取出一行数据,将行中取出 username 字段的值,并将其存入变量 $cookee 中,为设置Cookie做准备
其中还有个非常重要的函数 setcookie()
这是本篇的重点之一,一个非常重要的函数,用于向客户端的浏览器发送一个 Cookie
setcookie(
string $name,
string $value = "",
int $expires = 0,
string $path = "",
string $domain = "",
bool $secure = false,
bool $httponly = false,
array $options = []
): bool
参数详解
其中有很多参数都用不到,本示例也就用到了前三个参数
参数 | 说明 |
---|---|
$name | Cookie 的名称,这是唯一必须的参数 |
$value | Cookie 的值 |
$expires | Cookie 的过期时间 |
$path | Cookie 有效的服务器路径 |
$domain | Cookie 有效的域名/子域名 |
$secure | 安全性开关 |
$httponly | HTTP Only 开关 |
$options | (PHP 7.3+)一个关联数组 |
重要特性
- 基于 HTTP 头:
- setcookie() 函数实际上是告诉 PHP 在 HTTP 响应中发送一个 Set-Cookie 头
- 浏览器收到这个头后,会根据指令将 Cookie 存储起来
- 之后,浏览器向匹配路径和域名的服务器发送请求时,会自动在 HTTP 请求头中包含一个 Cookie 头,将信息传回服务器
注入点就出现在这:
这段代码的目的是:使用从用户Cookie中获取的用户名来查询数据库,验证该用户是否存在,即执行用户输入的内容(恶意代码)
实战演练
环境设置: 本示例为 sqli-labs 20
工具准备: Burp Suite
less-20和前面两个一样有安全绕过所以必须登录正确的用户名和密码,登陆成功后他才能把 cookie 插入到数据库中
输入正确的用户名密码回车后,进行抓包
username:admin
password:admin
并直接右键发送到重发器
$sql="SELECT * FROM users WHERE username='$cookee' LIMIT 0,1";
因为这里是单引号闭合并且是用union进行查询的,所以直接用单引号闭合的union查询即可
先使用 order by 判断列数
' order by 3 --+
构建注入语句,查询库名,不会的看这:sql整型注入
' union select 1,2,(select database()) --+
查询表名,后面的就不写了,跟sql整型注入一模一样
' union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema=database()) --+
防御之道:如何防御Cookie注入
在前面的Cookie注入的前置条件中就有提及,正所谓解铃还须系铃人,哪有漏洞补哪就行了
对Cookie进行安全编码(HttpOnly Flag):
- 作用: 防止XSS窃取。设置了HttpOnly的Cookie无法通过JavaScript的document.cookie访问
强制使用HTTPS(Secure Flag):
- 作用: 防止网络嗅探。设置了Secure的Cookie只会通过HTTPS加密连接传输
实施严格的输入验证和输出编码:
- 根源上解决XSS,从而杜绝最主要的Cookie窃取手段。对所有用户输入进行过滤和验证,对所有输出到页面的数据进行编码
服务器端不要信任客户端传来的Cookie:
- 核心原则: Cookie中的任何数据都应被视为不可信的输入
设置合适的SameSite属性:
- 作用: 可以有效防御CSRF攻击,而CSRF有时也会和Cookie利用相关联
定期更换SessionID:
- 作用: 即使Cookie被窃取,其有效期也很短,减小损失
- 时机: 用户登录后、登出时、重要操作(如修改密码)后