文章目录
引言
从一个大家熟悉的情景开始。比如:"大家在做爬虫或者测试API的时候,肯定都习惯性地修改过User-Agent 吧?为了让服务器认为我们是浏览器而不是脚本,我们经常会把它改成 Mozilla/5.0 ..."
但这个我们经常'伪造'的字段,如果被开发者不当处理,就会成为一个潜在的安全漏洞------User-Agent 注入攻击。今天,我们就来深入聊聊这个话题
阅读这篇博客后,你将了解User-Agent注入的原理、危害以及如何有效防御,无论是作为开发者还是安全爱好者都非常有价值
什么是User-Agent
User-Agent(用户代理) 是一个字符串(一串文本),用于标识发出请求的软件客户端。在互联网的语境下,它通常是你的网页浏览器向网站服务器发送的、用于标识自身身份的一串代码
你可以把它想象成一次网络访问的 "数字名片"
当你的浏览器(如 Chrome, Safari, Firefox)访问一个网站时,它会在每个HTTP请求的头部(Header)中包含这个User-Agent字符串。网站服务器收到后,就能知道是谁在访问它
User-Agent字符串的构成
一个典型的User-Agent字符串包含多个部分,提供了关于你的应用程序、操作系统和设备的信息。它的格式没有严格的标准,但通常遵循以下模式:
产品名称/版本号 (兼容性信息; 平台信息; 其他细节)
例如这个:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0
这就是再windows 11上的Edge浏览器的User-Agent
让我们把它拆开来看每一部分的含义:
-
Mozilla/5.0
- 含义:一个历史遗留的兼容性令牌。几乎所有现代浏览器都以此开头,以便被设计用于早期Netscape浏览器(代号Mozilla)的网站正确识别
-
(Windows NT 10.0; Win64; x64)
- 含义:描述了操作系统平台
- Windows NT 10.0: 代表Windows 10或Windows 11操作系统
- Win64 和 x64: 明确指出这是64位的Windows系统
-
AppleWebKit/537.36
- 含义 :指明浏览器使用的渲染引擎是AppleWebKit(版本537.36)。WebKit是Safari的渲染引擎,Chromium项目最初是基于它分支出来的,所以Chromium系的浏览器(包括Chrome和Edge)都保留了这个标识
-
(KHTML, like Gecko)
- 含义:同样是出于历史和兼容性原因而存在的令牌。KHTML是WebKit引擎的前身,Gecko是Firefox的引擎。这个语句表明它兼容这些引擎的标准
-
Chrome/139.0.0.0
- 含义 :这是最关键的信息之一 。它表明这个浏览器与Google Chrome 139版本兼容,并且基于相同的Chromium引擎。这解释了为什么Edge可以无缝运行所有为Chrome设计的扩展和网站
-
Safari/537.36
- 含义:再次提到Safari,是为了让网站认为它可能是一个Safari浏览器,以确保在那些为Safari优化的网站上也能获得最佳的兼容性。这里的版本号(537.36)与WebKit版本号对应
-
Edg/139.0.0.0
- 含义 :这是最关键的标识,指明了浏览器的真实身份是Microsoft Edge,版本号为139。浏览器总是把自己"真正"的名字放在最后
User-Agent的主要作用
-
内容协商(最核心的作用) :
网站服务器通过解析User-Agent来为你提供最合适的网页内容版本。
- 设备适配: 为桌面电脑、平板、手机或智能电视提供不同布局的页面(响应式设计有时也依赖于此)
- 浏览器兼容: 针对不同浏览器(如Chrome、IE)提供不同的代码,以确保功能正常。例如,过去会为老旧的IE浏览器提供特殊的CSS或JavaScript代码
- 功能支持: 检测浏览器是否支持某些特定功能(如WebP图片格式、特定视频编码等)
-
数据分析:
- 网站管理员使用User-Agent数据来生成统计报告,了解访客使用什么浏览器、什么操作系统、什么设备来访问他们的网站。这有助于他们决定要优先支持哪些平台和技术
-
机器人(Bots)识别:
- 搜索引擎爬虫(如Googlebot)、社交媒体机器人等在访问网站时也会发送其独特的User-Agent。网站可以通过这个来识别它们,并决定是允许其抓取内容还是将其拒之门外。同时,它也可以用来识别恶意的爬虫或扫描器
如何查看你自己的User-Agent
使用开发者工具(按F12键),在网络(Network)标签页中,点击任意一个请求,在"Headers"选项卡下找到"User-Agent "字段
既然说到这了,那顺便也提一下隐私问题 和 未来
- 隐私问题: 由于User-Agent包含了相当多的系统信息,它就像你的"浏览器指纹"的一部分,可以被用来在不同网站上跟踪你,即使你禁用了Cookie
- User-Agent冻结 : 为了减少指纹追踪并推动更标准的网络兼容性,主要浏览器厂商(如Chrome、Firefox)已经启动了"User-Agent冻结"计划。这意味着未来的浏览器版本将逐渐减少在User-Agent字符串中提供详细的版本号和操作系统信息,迫使开发者使用更现代、更隐私友好的方法(如特性检测)来适配网站,而不是依赖User-Agent嗅探
- User-Agent Client Hints: 这是取代传统User-Agent的新技术。它允许浏览器在默认情况下只分享必要的基本信息,如果服务器需要更多细节(如设备类型或完整版本),它必须主动向浏览器"请求"这些提示,浏览器可以选择是否提供
什么是User-Agent注入
核心定义
User-Agent注入 是一种网络安全攻击技术,属于HTTP头注入 (HTTP Header Injection)的一种。它发生在攻击者能够控制 或篡改 HTTP请求中的 User-Agent 头部,并在其中插入恶意的内容(如额外的HTTP头、换行符、命令等),从而欺骗服务器执行非预期的操作
其本质是应用程序没有对用户输入的 User-Agent 值进行严格的验证 、过滤 或转义,就将其不加处理地用于构建HTTP响应、记录日志或用于数据库查询,从而导致了安全漏洞
User-Agent注入是如何发生的?
一个典型的Web应用可能会出于各种目的记录或使用User-Agent信息,例如:
- 记录到日志文件中用于数据分析
- 在服务器端的数据库查询中(例如,统计不同浏览器的用户数量)
- 将User-Agent值直接输出到网页的管理后台(例如,显示最近登录的设备和浏览器)
如果应用程序只是简单地接收客户端发来的User-Agent字符串,而没有检查其内容是否合法,攻击者就可以伪造一个包含特殊字符的恶意User-Agent
简单来说就是:
当Web应用程序盲目信任 并未经验证/过滤就将User-Agent的值写入存储系统(如数据库、日志文件)或在页面中显示时,漏洞就产生了
User-Agent注入的前置条件
1. 应用程序信任并接收用户控制的User-Agent值
这是最根本的前提。攻击者必须能够控制 并修改 其浏览器或客户端发送的 User-Agent HTTP头。
- 如何实现:这通常非常简单。有大量浏览器插件(如「User-Agent Switcher」)、代理工具(如Burp Suite、OWASP ZAP)、或直接使用编程语言(如Python的requests库)都可以轻松地伪造任意User-Agent字符串
- 核心问题 :应用程序盲目信任 了来自客户端(不可信来源)的输入,违反了网络安全的基本原则------"永远不要信任用户输入"
2. 应用程序以不安全的方式使用该输入
仅仅能注入还不够,应用程序还必须以某种不安全的方式来处理这个被篡改的User-Agent字符串。常见的不安全使用场景包括:
- 未过滤地拼接入HTTP响应头 :这是导致 HTTP响应头拆分(HTTP Response Splitting) 的经典场景。如果应用程序将User-Agent值直接用于生成另一个HTTP响应头(例如在重定向、设置Cookie或自定义头中),并且没有过滤换行符,攻击者就能注入新的响应头
- 未转义地输出到HTML页面 :如果应用程序将User-Agent记录到数据库并在后台管理页面显示,或者直接在页面上显示"您的浏览器是:XXX",并且输出时没有进行HTML编码,就会导致 反射型或存储型XSS
- 未参数化地用于数据库查询 :如果应用程序将User-Agent字符串通过字符串拼接的方式写入SQL查询语句(例如记录访问日志),就会引入 SQL注入 的风险
- 未转义地用于系统命令 :在极少数情况下,如果应用程序将User-Agent传递给系统 shell 执行(例如调用一个命令行工具来分析日志),并且没有正确转义,可能导致 命令注入
3. 输入中允许包含关键特殊字符
攻击载荷必须包含能被目标系统解释的特殊字符。如果应用程序过滤了这些字符,攻击就会失效。最关键的特殊字符是:
-
换行符:
- 回车符(CR, %0d, \r)
- 换行符(LF, %0a, \n)
- 这两个字符是HTTP协议中用于分隔头部字段和标记头部结束的关键。允许注入CRLF (%0d%0a)是完成响应头拆分攻击的必要条件
-
特定语境下的元字符:
- 对于XSS :需要允许诸如 < , > , " , ' , & , / 等用于构造HTML标签和属性的字符
- 对于SQL注入 :需要允许诸如 ' , " , ; , -- , # , ) 等SQL语法中的元字符
- 对于命令注入 :需要允许诸如 | , & , ; , $() , > 等shell命令运算符
4. 注入位置具有相关性
即使成功注入了恶意内容,其位置也必须能产生实际影响
- 注入到响应头中:只有当注入点位于服务器生成的HTTP响应头部分时,CRLF注入才能生效。如果只是注入到HTML正文中,CRLF只会被显示为空白,不会改变HTTP结构(但可能造成XSS)
- 输出到特权页面:对于存储型XSS,被污染的User-Agent需要被显示在一个能被其他用户(尤其是管理员)访问的页面上,才能扩大攻击影响
我们可以将这些条件总结为一个攻击链,只有当整个链条畅通时,攻击才会成功:
攻击者能够控制输入 → 应用程序未经验证/过滤即使用 → 输入中包含特殊字符 → 应用程序在危险上下文中处理该输入 → 产生恶意后果
前置条件 | 攻击者视角 | 防御者视角(漏洞点) |
---|---|---|
1.输入可控 | 可轻易伪造User-Agent | 信任了不可信的客户端输入 |
2.使用方式不安全 | 发现应用程序会记录或使用UA | 代码逻辑存在安全隐患(拼接、直接输出) |
3.字符未过滤 | 测试发现换行符等特殊字符未被拦截 | 缺乏有效的输入验证和过滤机制 |
4.上下文相关 | 注入的代码在目标位置被执行 | 输出时没有根据上下文进行转义 |
因此,修复User-Agent注入漏洞的方法就是打破这个链条 ,通常是在第2和第3环节实施强有力的输入验证 、输出编码 和使用安全API(如参数化查询),后面在防御部分会着重强调
攻击原理与流程
攻击成功的链条非常简单:
- 应用程序读取:服务器端代码(如PHP、Node.js、ASP.NET)从HTTP请求中获取 User-Agent 的值
- PHP示例 : userAgent = _SERVER['HTTP_USER_AGENT'];
- 应用程序使用 :代码将这个值用于某个敏感操作 ,且未经过任何处理
- 攻击者篡改 :攻击者发送一个恶意构造的请求,其中包含精心设计的 User-Agent 值
- 恶意载荷执行:应用程序将恶意值带入敏感操作中,漏洞被触发
数据库查询 写入日志文件 输出到HTML页面 拼接到系统命令 用于重定向逻辑 攻击者伪造恶意User-Agent 应用程序读取并信任该值 应用程序如何使用该值? 导致SQL注入 导致日志污染/日志XSS 导致反射型XSS 导致命令注入RCE 导致CRLF注入
我们来挑一个示例详细介绍一下服务器为什么会造成 User-Agent注入
以最经典的less-18举例,审一下他的源码
php
<?php
//including the Mysql connect parameters.
include("../sql-connections/sql-connect.php");
error_reporting(0);
function check_input($value)
{
if(!empty($value))
{
// truncation (see comments)
$value = substr($value,0,20);
}
// Stripslashes if magic quotes enabled
if (get_magic_quotes_gpc())
{
$value = stripslashes($value);
}
// Quote if not a number
if (!ctype_digit($value))
{
$value = "'" . mysql_real_escape_string($value) . "'";
}
else
{
$value = intval($value);
}
return $value;
}
$uagent = $_SERVER['HTTP_USER_AGENT'];
$IP = $_SERVER['REMOTE_ADDR'];
echo "<br>";
echo 'Your IP ADDRESS is: ' .$IP;
echo "<br>";
//echo 'Your User Agent is: ' .$uagent;
// take the variables
if(isset($_POST['uname']) && isset($_POST['passwd']))
{
$uname = check_input($_POST['uname']);
$passwd = check_input($_POST['passwd']);
/*
echo 'Your Your User name:'. $uname;
echo "<br>";
echo 'Your Password:'. $passwd;
echo "<br>";
echo 'Your User Agent String:'. $uagent;
echo "<br>";
echo 'Your User Agent String:'. $IP;
*/
//logging the connection parameters to a file for analysis.
$fp=fopen('result.txt','a');
fwrite($fp,'User Agent:'.$uname."\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";
$result1 = mysql_query($sql);
$row1 = mysql_fetch_array($result1);
if($row1)
{
echo '<font color= "#FFFF00" font size = 3 >';
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
mysql_query($insert);
//echo 'Your IP ADDRESS is: ' .$IP;
echo "</font>";
//echo "<br>";
echo '<font color= "#0000ff" font size = 3 >';
echo 'Your User Agent is: ' .$uagent;
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>";
}
}
?>
总结一下最重要的也就这几行代码

php
$uagent = $_SERVER['HTTP_USER_AGENT'];
这行代码直接从HTTP请求头中获取User-Agent值,没有进行任何验证或过滤
- $_SERVER: 这是一个 PHP 超全局数组,包含了服务器和客户端请求的相关信息(如头信息、路径、脚本位置等)
- $_SERVER['HTTP_USER_AGENT'] : 这个数组元素包含了客户端(通常是浏览器)发送的 User-Agent HTTP 头字符串,就是上面所讲的 User-Agent
- **uagent = ...**: 将这个 User-Agent 字符串赋值给变量 uagent,以便在代码后面使用
php
$IP = $_SERVER['REMOTE_ADDR'];
echo "<br>";
echo 'Your IP ADDRESS is: ' .$IP;
echo "<br>";
这段代码表示了与服务器建立连接的客户端的 IP 地址 ,并在后面输出出来,这就是页面所看到 IP 的由来

这行代码是注入的关键
php
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
这行代码的作用是将提取出来的 uagent, IP, $uname 这些字段插入到 security 数据库的 uagents 表下
又因为 $uagent 这个字段没有任何过滤、转义或使用参数化查询,所以存在注入
那咱来分析一下存在注入漏洞的原因:
- 直接拼接用户输入 :代码将变量 uagent, IP, $uname 直接拼接到SQL语句中。如果这些变量包含特殊SQL字符(如单引号 ' 、分号 ; 、注释 --+ 等),攻击者可以操纵SQL查询的结构
- 缺乏输入验证和转义:没有处理用户输入,导致恶意输入被直接执行
实战演练
环境设置 : 本示例为 sqli-labs 18
工具准备 : Burp Suite
因为less-18有安全绕过所以必须登录正确的用户名和密码,登陆成功后他才能把 User-Agent 插入到数据库中
开启Burp Suite,配置代理后,打开拦截
在输入框中输入正确的用户名和密码,回车
这就是拦截后的内容
右键Burp Suite,将数据发送到 Repeater ,以方便后续操作
发送到Repeater后,我们要更改的就是 User-Agent 部分,也就是本期主角
由于是插入数据,所以我们用报错注入,updatexml()报错注入,extractValue()报错注入,floor()报错注入都可以,这里就用更简单的 updatexml()注入 了
php
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent', '$IP', $uname)";
因为这条命令里的 uagent** 字段存在漏洞,所以注入点就在这里,既然 **uagent 获取的是User-Agent值,所以我们更改User-Agent内容即可
构建注入语句,查询库名
php
' or updatexml(1,concat('~',(select database())),3),2,3) #
查询表名,列名,只需要将 select database() 替换了即可,由于篇幅有限,这里就不详细写了,不清楚的可以看updatexml()报错注入里面有详细过程
防御之道:如何防止User-Agent报头注入
- 黄金法则:永远不要信任用户输入!(包括HTTP头)
- 具体措施 :
-
输入验证与过滤:
- 严格定义UA的合法字符集(白名单),比如只允许字母、数字、空格和特定符号
- 对长度进行限制
- 过滤或编码所有恶意字符(如 < , > , ' , " , \r , \n)
-
输出编码:
- 如果一定要在HTML中显示UA,必须进行HTML编码(例如PHP的 htmlspecialchars() , Python的 html.escape())
- 如果写入数据库,使用参数化查询(Prepared Statements)来彻底杜绝SQL注入,而不是拼接字符串
- 如果写入日志,考虑对换行符进行转义
-
使用安全的库和框架:
- 现代Web框架(如Django, Spring, Laravel)通常内置了良好的安全机制,使用它们提供的标准方式获取和處理输入数据
-