Web安全:深入理解User-Agent报头注入与防御

文章目录

引言

从一个大家熟悉的情景开始。比如:"大家在做爬虫或者测试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

让我们把它拆开来看每一部分的含义:

  1. Mozilla/5.0

    • 含义:一个历史遗留的兼容性令牌。几乎所有现代浏览器都以此开头,以便被设计用于早期Netscape浏览器(代号Mozilla)的网站正确识别
  2. (Windows NT 10.0; Win64; x64)

    • 含义:描述了操作系统平台
    • Windows NT 10.0: 代表Windows 10或Windows 11操作系统
    • Win64x64: 明确指出这是64位的Windows系统
  3. AppleWebKit/537.36

    • 含义 :指明浏览器使用的渲染引擎是AppleWebKit(版本537.36)。WebKit是Safari的渲染引擎,Chromium项目最初是基于它分支出来的,所以Chromium系的浏览器(包括Chrome和Edge)都保留了这个标识
  4. (KHTML, like Gecko)

    • 含义:同样是出于历史和兼容性原因而存在的令牌。KHTML是WebKit引擎的前身,Gecko是Firefox的引擎。这个语句表明它兼容这些引擎的标准
  5. Chrome/139.0.0.0

    • 含义 :这是最关键的信息之一 。它表明这个浏览器与Google Chrome 139版本兼容,并且基于相同的Chromium引擎。这解释了为什么Edge可以无缝运行所有为Chrome设计的扩展和网站
  6. Safari/537.36

    • 含义:再次提到Safari,是为了让网站认为它可能是一个Safari浏览器,以确保在那些为Safari优化的网站上也能获得最佳的兼容性。这里的版本号(537.36)与WebKit版本号对应
  7. Edg/139.0.0.0

    • 含义 :这是最关键的标识,指明了浏览器的真实身份是Microsoft Edge,版本号为139。浏览器总是把自己"真正"的名字放在最后

User-Agent的主要作用

  1. 内容协商(最核心的作用) :

    网站服务器通过解析User-Agent来为你提供最合适的网页内容版本。

    • 设备适配: 为桌面电脑、平板、手机或智能电视提供不同布局的页面(响应式设计有时也依赖于此)
    • 浏览器兼容: 针对不同浏览器(如Chrome、IE)提供不同的代码,以确保功能正常。例如,过去会为老旧的IE浏览器提供特殊的CSS或JavaScript代码
    • 功能支持: 检测浏览器是否支持某些特定功能(如WebP图片格式、特定视频编码等)
  2. 数据分析:

    • 网站管理员使用User-Agent数据来生成统计报告,了解访客使用什么浏览器、什么操作系统、什么设备来访问他们的网站。这有助于他们决定要优先支持哪些平台和技术
  3. 机器人(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信息,例如:

  1. 记录到日志文件中用于数据分析
  2. 在服务器端的数据库查询中(例如,统计不同浏览器的用户数量)
  3. 将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(如参数化查询),后面在防御部分会着重强调


攻击原理与流程

攻击成功的链条非常简单:

  1. 应用程序读取:服务器端代码(如PHP、Node.js、ASP.NET)从HTTP请求中获取 User-Agent 的值
  2. PHP示例 : userAgent = _SERVER['HTTP_USER_AGENT'];
  3. 应用程序使用 :代码将这个值用于某个敏感操作 ,且未经过任何处理
  4. 攻击者篡改 :攻击者发送一个恶意构造的请求,其中包含精心设计的 User-Agent
  5. 恶意载荷执行:应用程序将恶意值带入敏感操作中,漏洞被触发

数据库查询 写入日志文件 输出到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 这个字段没有任何过滤、转义或使用参数化查询,所以存在注入

那咱来分析一下存在注入漏洞的原因

  1. 直接拼接用户输入 :代码将变量 uagent, IP, $uname 直接拼接到SQL语句中。如果这些变量包含特殊SQL字符(如单引号 ' 、分号 ; 、注释 --+ 等),攻击者可以操纵SQL查询的结构
  2. 缺乏输入验证和转义:没有处理用户输入,导致恶意输入被直接执行

实战演练

环境设置 : 本示例为 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头)
  • 具体措施
    1. 输入验证与过滤

      • 严格定义UA的合法字符集(白名单),比如只允许字母、数字、空格和特定符号
      • 对长度进行限制
      • 过滤或编码所有恶意字符(如 < , > , ' , " , \r , \n
    2. 输出编码

      • 如果一定要在HTML中显示UA,必须进行HTML编码(例如PHP的 htmlspecialchars() , Python的 html.escape()
      • 如果写入数据库,使用参数化查询(Prepared Statements)来彻底杜绝SQL注入,而不是拼接字符串
      • 如果写入日志,考虑对换行符进行转义
    3. 使用安全的库和框架

      • 现代Web框架(如Django, Spring, Laravel)通常内置了良好的安全机制,使用它们提供的标准方式获取和處理输入数据